@join-pantheon/cli 0.32.1 → 0.33.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.
Binary file
@@ -8,13 +8,44 @@
8
8
  # before this script can explain: there, use the `irm | iex` forms)
9
9
  #
10
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"
11
+ # powershell -NoProfile -Command "$env:PANTHEON_ARGS='join <invite link>'; irm https://relay.joinpantheon.network/install.ps1 | iex"
12
12
  #
13
13
  # One line for either window (what `pantheon invite` prints): PowerShell would
14
14
  # expand the $env: above inside the outer double quotes before the inner
15
15
  # PowerShell runs, so this sets the variable without a $, and with a cmdlet
16
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"
17
+ # powershell -NoProfile -Command "Set-Item Env:PANTHEON_ARGS 'join <invite link>'; irm https://relay.joinpantheon.network/install.ps1 | iex"
18
+ #
19
+ # Downloaded first (what a coding agent is asked to do, so it can read it), then
20
+ # run from the file the same way, from Git Bash, cmd.exe or PowerShell:
21
+ # curl.exe -fsSL https://relay.joinpantheon.network/install.ps1 -o pantheon-install.ps1
22
+ # powershell -NoProfile -Command "Set-Item Env:PANTHEON_ARGS 'join <invite link>'; Get-Content -Raw pantheon-install.ps1 | iex"
23
+ #
24
+ # None of these needs -ExecutionPolicy Bypass: the execution policy governs
25
+ # running a script FILE (-File, .\script.ps1), and iex runs a string, which no
26
+ # policy looks at, Restricted included. And iex, unlike [scriptblock]::Create,
27
+ # still runs under Constrained Language Mode far enough for the check below to
28
+ # say why the install cannot go on.
29
+ #
30
+ # What Pantheon is: a command-line tool (npm: @join-pantheon/cli) that lets your
31
+ # coding agents (Claude Code, Cursor, Codex, GitHub Copilot, Kiro) hand work and
32
+ # questions to your teammates' agents. Their messages pass through Pantheon's
33
+ # hosted relay, relay.joinpantheon.network, which this computer connects out to
34
+ # as a client, the way a chat app does. This computer runs no relay, opens no
35
+ # port to the network and carries no one else's traffic. (`pantheon board`,
36
+ # when you open it, listens on 127.0.0.1 only, for this computer's own browser.
37
+ # Hosting a relay is a separate, explicit choice: `pantheon relay`.)
38
+ #
39
+ # `start` creates a team; `join` joins one. Either signs you in with a 6-digit
40
+ # code sent to your email, adds a "pantheon" MCP server entry and a session hook
41
+ # to the coding agents it finds. By default it also sets up, for you alone and
42
+ # with no admin rights, one Task Scheduler task ("Pantheon") that starts a
43
+ # hidden background host when you log on. The host runs a worker that can run
44
+ # your own coding agent on a teammate's request while you are away (in a
45
+ # separate copy of the repo, delivering on a branch for your review), desktop
46
+ # alerts (Windows notifications), and a tray icon that opens `pantheon board`.
47
+ # Leave any of it out with --no-worker or --no-notifications; remove it all with
48
+ # pantheon uninstall.
18
49
  #
19
50
  # `irm | iex` cannot pass arguments, so the arguments for pantheon travel in
20
51
  # $env:PANTHEON_ARGS, which reads the same from PowerShell and from cmd.exe.
@@ -98,6 +129,127 @@ function Install-Pantheon {
98
129
  [IO.File]::WriteAllText($LogFile, $r.out + $r.err)
99
130
  return $r.code
100
131
  }
132
+ # This user's logon task: "Pantheon-<SID>" (src/win-host.ts taskNameFor), and
133
+ # "Pantheon", the name builds before 0.33.0 gave it.
134
+ function Get-PantheonTaskNames {
135
+ $names = @()
136
+ try { $names += 'Pantheon-' + [Security.Principal.WindowsIdentity]::GetCurrent().User.Value } catch { $names = @() }
137
+ $names += 'Pantheon'
138
+ return , $names
139
+ }
140
+
141
+ # The processes running a program from inside $Dir: what keeps Windows from
142
+ # renaming it. Each as @{ id; name; line } (line: its command line, when Windows says).
143
+ function Get-FolderHolders([string]$Dir) {
144
+ $prefix = $Dir.TrimEnd('\') + '\'
145
+ $out = @()
146
+ foreach ($p in @(Get-Process -ErrorAction SilentlyContinue)) {
147
+ $path = $null
148
+ try { $path = $p.Path } catch { $path = $null }
149
+ if (-not $path -or -not $path.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { continue }
150
+ $line = ''
151
+ try { $line = [string](Get-CimInstance Win32_Process -Filter ('ProcessId=' + $p.Id) -ErrorAction Stop).CommandLine } catch { $line = '' }
152
+ $out += , @{ id = $p.Id; name = ($p.ProcessName + '.exe'); line = $line }
153
+ }
154
+ return , $out
155
+ }
156
+
157
+ # What holds a folder, for a person to read: "node.exe (pid 4242: ...cli.js mcp)".
158
+ function Format-Holders($Holders) {
159
+ (@($Holders) | ForEach-Object {
160
+ $l = [string]$_.line
161
+ if ($l.Length -gt 90) { $l = '...' + $l.Substring($l.Length - 87) }
162
+ if ($l) { '{0} (pid {1}: {2})' -f $_.name, $_.id, $l } else { '{0} (pid {1})' -f $_.name, $_.id }
163
+ }) -join ', '
164
+ }
165
+
166
+ # The woken run in flight in this Pantheon home, as src/worker.ts runInFlight
167
+ # reads it: the pid in worker-run.json while the record is fresh (a run lasts
168
+ # at most 20 minutes; 5 more of slack) and that process is alive. $null when
169
+ # there is none, or the record cannot be read.
170
+ function Get-PantheonRunInFlight([string]$RunRecord, [scriptblock]$Alive) {
171
+ if (-not $RunRecord -or -not (Test-Path -LiteralPath $RunRecord -PathType Leaf)) { return $null }
172
+ $runPid = 0; $at = 0.0
173
+ try {
174
+ $r = [IO.File]::ReadAllText($RunRecord) | ConvertFrom-Json
175
+ $runPid = [int]$r.pid; $at = [double]$r.at
176
+ } catch { return $null }
177
+ if ($runPid -le 0 -or $at -le 0) { return $null }
178
+ $now = [double][DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
179
+ if ($at -gt $now + 60000 -or $now - $at -gt 25 * 60000) { return $null }
180
+ $up = $false
181
+ try { $up = [bool](& $Alive $runPid) } catch { $up = $false }
182
+ if (-not $up) { return $null }
183
+ return $runPid
184
+ }
185
+
186
+ # Is a task this user's? Its XML (schtasks /Query /TN <name> /XML) names its
187
+ # principal as DOMAIN\user, a bare user name, or a SID; every one of them must
188
+ # be this user (src/win-host.ts taskIsUsers). A task of the same name that
189
+ # another person on this PC registered is never ended.
190
+ function Test-PantheonTaskIsMine([string]$Xml, [string]$UserId, [string]$Sid) {
191
+ if (-not $Xml) { return $false }
192
+ $ids = @([regex]::Matches(($Xml -replace "`0", ''), '<UserId>([^<]*)</UserId>') | ForEach-Object { $_.Groups[1].Value.Trim().ToLowerInvariant() })
193
+ if ($ids.Count -eq 0) { return $false }
194
+ $u = ([string]$UserId).ToLowerInvariant()
195
+ $s = ([string]$Sid).ToLowerInvariant()
196
+ $bare = if ($u.Contains('\')) { $u.Substring($u.LastIndexOf('\') + 1) } else { $u }
197
+ foreach ($id in $ids) {
198
+ if ($u -and $id -eq $u) { continue }
199
+ if ($s -and $id -eq $s) { continue }
200
+ if ($bare -and -not $id.Contains('\') -and -not $id.StartsWith('s-1-') -and $id -eq $bare) { continue }
201
+ return $false
202
+ }
203
+ return $true
204
+ }
205
+
206
+ # Before the runtime folder is swapped: Pantheon's background host (the
207
+ # logon task) runs node.exe from it, and Windows will not rename a folder a
208
+ # running program came from. A teammate's request the worker is running
209
+ # (worker-run.json) is waited for first, up to $RunWaitSeconds, and while it
210
+ # still runs nothing is stopped at all: busy comes back, and the runtime stays
211
+ # as it is. Then the host is asked to stop the way `pantheon stop all` asks it
212
+ # (<home>\host\stop), given up to $WaitSeconds to let go, and only then are
213
+ # its tasks ended -- those whose principal is this user ($TaskIsMine), never a
214
+ # task of the same name someone else on this PC registered. Returns
215
+ # @{ restart; holders; busy; tasks }: whether a task is to be started again
216
+ # once the install is done (tasks: which, this user's), and what still holds
217
+ # the folder (an agent window's Pantheon connection, which is not ours to end).
218
+ function Stop-PantheonHost {
219
+ param([string]$NodeDir, [string]$HostDir, [string[]]$Tasks, [scriptblock]$Holders, [scriptblock]$EndTask, [int]$WaitSeconds = 15,
220
+ [string]$RunRecord = '', [scriptblock]$RunAlive = { param($p) $null -ne (Get-Process -Id $p -ErrorAction SilentlyContinue) },
221
+ [int]$RunWaitSeconds = 180, [scriptblock]$TaskIsMine = { param($t) $true }, [scriptblock]$Say = { param($m) Write-Host $m })
222
+ $left = @(& $Holders $NodeDir)
223
+ if ($left.Count -eq 0) { return @{ restart = $false; holders = @(); busy = $false; tasks = @() } }
224
+ if (Get-PantheonRunInFlight $RunRecord $RunAlive) {
225
+ & $Say "A teammate's request is running here; waiting for it to finish before updating (up to $([math]::Ceiling($RunWaitSeconds / 60)) minutes)..."
226
+ $until = (Get-Date).AddSeconds($RunWaitSeconds)
227
+ $running = $true
228
+ while ($running -and (Get-Date) -lt $until) {
229
+ Start-Sleep -Milliseconds 1000
230
+ $running = [bool](Get-PantheonRunInFlight $RunRecord $RunAlive)
231
+ }
232
+ if ($running) { return @{ restart = $false; holders = $left; busy = $true; tasks = @() } }
233
+ $left = @(& $Holders $NodeDir)
234
+ if ($left.Count -eq 0) { return @{ restart = $false; holders = @(); busy = $false; tasks = @() } }
235
+ }
236
+ $hostUp = Test-Path -LiteralPath (Join-Path $HostDir 'state.json')
237
+ if ($hostUp) {
238
+ try { [IO.File]::WriteAllText((Join-Path $HostDir 'stop'), (Get-Date).ToString('o')) } catch { $hostUp = $false }
239
+ }
240
+ $deadline = (Get-Date).AddSeconds($WaitSeconds)
241
+ while ($hostUp -and $left.Count -gt 0 -and (Get-Date) -lt $deadline) {
242
+ Start-Sleep -Milliseconds 500
243
+ $left = @(& $Holders $NodeDir)
244
+ }
245
+ $mine = @($Tasks | Where-Object { $_ } | Where-Object { $ok = $false; try { $ok = [bool](& $TaskIsMine $_) } catch { $ok = $false }; $ok })
246
+ if ($left.Count -gt 0) {
247
+ foreach ($t in $mine) { & $EndTask $t }
248
+ for ($i = 0; $i -lt 6 -and $left.Count -gt 0; $i++) { Start-Sleep -Milliseconds 500; $left = @(& $Holders $NodeDir) }
249
+ }
250
+ return @{ restart = $hostUp; holders = $left; busy = $false; tasks = $mine }
251
+ }
252
+
101
253
  # The same, with the output handed back instead: @{ code; out; err }.
102
254
  function Invoke-Quiet([string]$Exe, [string[]]$Items) {
103
255
  $psi = New-Object System.Diagnostics.ProcessStartInfo
@@ -170,6 +322,8 @@ function Install-Pantheon {
170
322
  $binDir = Join-Path $root 'bin'
171
323
  $launcher = Join-Path $binDir 'pantheon.cmd'
172
324
  $log = Join-Path $root 'install.log'
325
+ $pantheonHomeDir = if ($env:PANTHEON_HOME) { $env:PANTHEON_HOME } else { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.pantheon' }
326
+ $hostDir = Join-Path $pantheonHomeDir 'host'
173
327
 
174
328
  $State.step = "creating $root"
175
329
  try { New-Item -ItemType Directory -Force -Path $runtime, $binDir | Out-Null }
@@ -212,6 +366,21 @@ function Install-Pantheon {
212
366
  Fail "could not download $Url ($last). Check your internet connection; behind a proxy set HTTPS_PROXY; then run this again."
213
367
  }
214
368
 
369
+ # The version of the CLI in the runtime, but only when it is whole: its
370
+ # package.json names that version AND it starts and says so. A run stopped
371
+ # halfway through npm can leave the first without the second; that is
372
+ # installed again, never called done. (Defined before step 1: a run that
373
+ # stops there says which version is still in place.)
374
+ $manifest = Join-Path $pkgDir 'package.json'
375
+ $installedCli = {
376
+ $v = ''
377
+ if ((Test-Path -LiteralPath $manifest) -and (Test-Path -LiteralPath $entry)) {
378
+ try { $v = [string](Get-Content -LiteralPath $manifest -Raw | ConvertFrom-Json).version } catch { $v = '' }
379
+ if ($v -and (Invoke-Quiet $nodeExe @($entry, '--version')).out.Trim() -ne $v) { $v = '' }
380
+ }
381
+ $v
382
+ }
383
+
215
384
  # ---- 1. Pantheon's own Node --------------------------------------------
216
385
 
217
386
  $haveNode = ''
@@ -278,6 +447,22 @@ function Install-Pantheon {
278
447
  $State.step = 'putting Node in place'
279
448
  $aside = $null
280
449
  if (Test-Path -LiteralPath $nodeDir) {
450
+ # Pantheon's background host runs from this folder: stopped first
451
+ # (and started again at the end), or the folder cannot be moved.
452
+ $State.step = "stopping Pantheon's background host"
453
+ $schtasks = Join-Path $env:SystemRoot 'System32\schtasks.exe'
454
+ $tasks = Get-PantheonTaskNames
455
+ $meUser = ''; $meSid = ''
456
+ try { $meId = [Security.Principal.WindowsIdentity]::GetCurrent(); $meUser = $meId.Name; $meSid = $meId.User.Value } catch { $meUser = $env:USERNAME }
457
+ $stopped = Stop-PantheonHost -NodeDir $nodeDir -HostDir $hostDir -Tasks $tasks `
458
+ -Holders { param($d) Get-FolderHolders $d } -EndTask { param($t) $null = Invoke-Quiet $schtasks @('/End', '/TN', $t) } `
459
+ -RunRecord (Join-Path $pantheonHomeDir 'worker-run.json') `
460
+ -TaskIsMine { param($t) Test-PantheonTaskIsMine (Invoke-Quiet $schtasks @('/Query', '/TN', $t, '/XML')).out $meUser $meSid }
461
+ # A teammate's run still going after the wait: nothing was stopped,
462
+ # and the runtime is left exactly as it is (see the catch below).
463
+ if ($stopped.busy) { throw 'PANTHEON_BUSY:' }
464
+ if ($stopped.restart) { $State.restartTasks = @($stopped.tasks); Say "Stopped Pantheon's background host while its Node is replaced; it starts again when this is done." }
465
+ $State.step = 'putting Node in place'
281
466
  $aside = Join-Path $tmp 'old'
282
467
  # A virus scanner looking at the new or old node.exe holds it for a
283
468
  # moment: a few tries before telling the person something is open.
@@ -285,7 +470,11 @@ function Install-Pantheon {
285
470
  for ($i = 1; $i -le 5 -and -not $moved; $i++) {
286
471
  try { Move-Item -LiteralPath $nodeDir -Destination $aside; $moved = $true } catch { Start-Sleep -Seconds 2 }
287
472
  }
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." }
473
+ if (-not $moved) {
474
+ $held = Get-FolderHolders $nodeDir
475
+ $what = if (@($held).Count) { 'in use by ' + (Format-Holders $held) + '.' } else { 'in use (by a program Windows did not name; a virus scanner holds it for a moment).' }
476
+ Fail "Pantheon's Node in $nodeDir is $what Close your agent windows (Claude Code, Cursor, VS Code) and any pantheon command, then run this again."
477
+ }
289
478
  }
290
479
  $placed = $false; $lastErr = ''
291
480
  for ($i = 1; $i -le 5 -and -not $placed; $i++) {
@@ -308,20 +497,6 @@ function Install-Pantheon {
308
497
 
309
498
  # ---- 2. the Pantheon CLI, with that Node's own npm -----------------------
310
499
 
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
500
  # What npm installs. PANTHEON_INSTALL_SPEC swaps the registry package for a
326
501
  # package file on this machine, so a release candidate goes through this
327
502
  # exact script before its version is on npm. Only a full path on a drive
@@ -387,6 +562,8 @@ function Install-Pantheon {
387
562
  $cmdText = "@echo off`r`n" +
388
563
  "rem The pantheon command, written by the Pantheon installer. It runs Pantheon's own`r`n" +
389
564
  "rem Node by absolute path, so it never depends on which node comes first on PATH.`r`n" +
565
+ 'rem An upgrade cut off mid-swap leaves the previous version beside the runtime: put it back.' + "`r`n" +
566
+ 'if not exist "%~dp0..\runtime\node\node_modules\@join-pantheon\cli\" if exist "%~dp0..\runtime\cli.prev\bin\pantheon.js" move "%~dp0..\runtime\cli.prev" "%~dp0..\runtime\node\node_modules\@join-pantheon\cli" >nul 2>&1' + "`r`n" +
390
567
  "`"%~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
568
  $current = if (Test-Path -LiteralPath $launcher) { [IO.File]::ReadAllText($launcher) } else { '' }
392
569
  if ($current -ne $cmdText) {
@@ -428,6 +605,9 @@ function Install-Pantheon {
428
605
 
429
606
  $State.lock.Dispose()
430
607
  $State.lock = $null
608
+ # The background host stopped for the swap starts again, on the new Node.
609
+ Start-PantheonTasks $State
610
+ $State.step = 'starting pantheon'
431
611
  if ($Arguments.Count -eq 0) {
432
612
  Say ''
433
613
  Say "Pantheon $PantheonVersion is installed. Open a new terminal, then:"
@@ -442,7 +622,10 @@ function Install-Pantheon {
442
622
  # Start-Process -NoNewWindow, so pantheon gets this console itself: a real
443
623
  # terminal for the sign-in questions, not a pipe PowerShell reads from.
444
624
  $State.step = 'starting pantheon'
445
- $line = ConvertTo-ArgLine (@($entry) + $Arguments)
625
+ # With the same --use-system-ca as npm and pantheon.cmd: this first run is the
626
+ # one that signs in, and behind a TLS-inspecting proxy the relay's
627
+ # certificate chains only to the company root in the Windows store.
628
+ $line = ConvertTo-ArgLine (@($ca) + @($entry) + $Arguments)
446
629
  $noConsole = $false
447
630
  try { $noConsole = [Console]::IsInputRedirected } catch { $noConsole = $false }
448
631
  if (-not $noConsole) {
@@ -490,6 +673,16 @@ function Install-Pantheon {
490
673
  }
491
674
  } catch {
492
675
  $m = $_.Exception.Message
676
+ if ($m -like 'PANTHEON_BUSY:*') {
677
+ # Not a failure: the Pantheon already here keeps working. Only when there
678
+ # is none that runs is the exit code non-zero.
679
+ $have = ''
680
+ if ($installedCli) { try { $have = & $installedCli } catch { $have = '' } }
681
+ $kept = if ($have) { " Pantheon $have is still installed and working." } else { '' }
682
+ Write-Host "Pantheon was not updated because a teammate's run was in progress; run the same line again in a few minutes.$kept" -ForegroundColor Yellow
683
+ $State.code = if ($have) { 0 } else { 1 }
684
+ return
685
+ }
493
686
  if ($m -like 'PANTHEON_FAIL:*') { $m = $m.Substring('PANTHEON_FAIL:'.Length) }
494
687
  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
688
  Write-Host "pantheon install failed: $m" -ForegroundColor Red
@@ -497,7 +690,33 @@ function Install-Pantheon {
497
690
  } finally {
498
691
  # Every way out, Ctrl-C included, lets go of the install lock.
499
692
  if ($State.lock) { $State.lock.Dispose(); $State.lock = $null }
693
+ # And starts again a background host this install stopped (on whichever Node is in place).
694
+ Start-PantheonTasks $State
695
+ }
696
+ }
697
+
698
+ # Start again the logon task Stop-PantheonHost stopped: this user's first task
699
+ # that starts. Once only. Its host removes the stop request when it starts.
700
+ function Start-PantheonTasks([hashtable]$State) {
701
+ if (-not $State.ContainsKey('restartTasks') -or $State.restarted) { return }
702
+ $State.restarted = $true
703
+ $schtasks = Join-Path $env:SystemRoot 'System32\schtasks.exe'
704
+ foreach ($t in $State.restartTasks) {
705
+ try {
706
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
707
+ $psi.FileName = $schtasks
708
+ $psi.Arguments = '/Run /TN "' + $t + '"'
709
+ $psi.UseShellExecute = $false
710
+ $psi.CreateNoWindow = $true
711
+ $psi.RedirectStandardOutput = $true
712
+ $psi.RedirectStandardError = $true
713
+ $p = [System.Diagnostics.Process]::Start($psi)
714
+ $null = $p.StandardOutput.ReadToEnd(); $null = $p.StandardError.ReadToEnd()
715
+ $p.WaitForExit()
716
+ if ($p.ExitCode -eq 0) { Write-Host "Started Pantheon's background host again ($t)."; return }
717
+ } catch { }
500
718
  }
719
+ Write-Host "note: Pantheon's background host did not start again by itself; it starts at your next logon, or now with: pantheon service start worker" -ForegroundColor Yellow
501
720
  }
502
721
 
503
722
  # `join '<link>'` as two arguments: whitespace splits, and single or double
@@ -548,7 +767,7 @@ $PantheonInstallCode = [int]$PantheonInstallState.code
548
767
  # result is left in $LASTEXITCODE instead.
549
768
  $PantheonOneShot = $false
550
769
  try { $PantheonOneShot = Test-PantheonOneShot ([Environment]::GetCommandLineArgs()) } catch { $PantheonOneShot = $false }
551
- Remove-Item Function:Install-Pantheon, Function:Split-PantheonArgs, Function:Test-PantheonOneShot -ErrorAction SilentlyContinue
770
+ Remove-Item Function:Install-Pantheon, Function:Start-PantheonTasks, Function:Split-PantheonArgs, Function:Test-PantheonOneShot -ErrorAction SilentlyContinue
552
771
  Remove-Variable PantheonInstallArgs, PantheonInstallState -ErrorAction SilentlyContinue
553
772
  $global:LASTEXITCODE = $PantheonInstallCode
554
773
  if ($PantheonOneShot) { exit $PantheonInstallCode }
@@ -5,6 +5,25 @@
5
5
  # curl -fsSL https://relay.joinpantheon.network/install.sh | sh -s -- start
6
6
  # curl -fsSL https://relay.joinpantheon.network/install.sh | sh (install only)
7
7
  #
8
+ # What Pantheon is: a command-line tool (npm: @join-pantheon/cli) that lets your
9
+ # coding agents (Claude Code, Cursor, Codex, GitHub Copilot, Kiro) hand work and
10
+ # questions to your teammates' agents. Their messages pass through Pantheon's
11
+ # hosted relay, relay.joinpantheon.network, which this computer connects out to
12
+ # as a client, the way a chat app does. This computer runs no relay, opens no
13
+ # port to the network and carries no one else's traffic. (`pantheon board`,
14
+ # when you open it, listens on 127.0.0.1 only, for this computer's own browser.
15
+ # Hosting a relay is a separate, explicit choice: `pantheon relay`.)
16
+ #
17
+ # `start` creates a team; `join` joins one. Either signs you in with a 6-digit
18
+ # code sent to your email, adds a "pantheon" MCP server entry and a session hook
19
+ # to the coding agents it finds, and by default sets up background helpers, each
20
+ # of which can be left out (--no-worker, --no-menubar, --no-notifications): a
21
+ # worker that can run your own coding agent on a teammate's request while you
22
+ # are away, in a separate copy of the repo, delivering on a branch for your
23
+ # review; desktop alerts (on macOS, and on a Linux desktop through its own
24
+ # notifications); and the menu bar on macOS. On Linux, `pantheon board` shows
25
+ # the same in a browser tab.
26
+ #
8
27
  # What it does, and nothing else:
9
28
  # 1. downloads one pinned Node release from nodejs.org, checks its sha256
10
29
  # against the sha256 pinned in this file (nodejs.org's own), and unpacks it into
@@ -428,6 +447,8 @@ STEP="writing $LAUNCHER"
428
447
  printf '# The pantheon command, written by the Pantheon installer. It runs\n'
429
448
  printf "# Pantheon's own Node by absolute path, so it never depends on which\n"
430
449
  printf '# node comes first on PATH. Running the installer again rewrites it.\n'
450
+ printf '# An upgrade cut off mid-swap leaves the previous version beside the runtime: put it back.\n'
451
+ printf '[ -e %s ] || { [ -f %s ] && mv %s %s 2>/dev/null; }\n' "$(shq "$PKG_DIR")" "$(shq "$RUNTIME/cli.prev/bin/pantheon.js")" "$(shq "$RUNTIME/cli.prev")" "$(shq "$PKG_DIR")"
431
452
  printf 'exec %s %s%s "$@"\n' "$(shq "$NODE")" "${ca:+$ca }" "$(shq "$PKG_DIR/bin/pantheon.js")"
432
453
  } >"$TMP_DIR/launcher" || fail "could not write the pantheon command. Check free disk space, then run this again."
433
454
  chmod 755 "$TMP_DIR/launcher"
@@ -487,6 +508,17 @@ add_block() {
487
508
  note "the pantheon block in $f has a marker line without its pair, so $f was left as it is; put $BIN_DIR at the front of PATH there yourself"
488
509
  return 0
489
510
  fi
511
+ # One of each, but the end before the begin (edited by hand): the rewrite
512
+ # below would skip from the begin marker to the end of the file and drop
513
+ # everything after it, so the file is not touched either.
514
+ if [ "${begins:-0}" -eq 1 ]; then
515
+ bline=$(grep -nxF "$BEGIN_MARK" "$f" | head -n 1 | cut -d: -f1)
516
+ eline=$(grep -nxF "$END_MARK" "$f" | head -n 1 | cut -d: -f1)
517
+ if [ "${bline:-0}" -gt "${eline:-0}" ]; then
518
+ note "the pantheon block in $f has its end marker before its begin marker, so $f was left as it is; put $BIN_DIR at the front of PATH there yourself"
519
+ return 0
520
+ fi
521
+ fi
490
522
  if [ "${begins:-0}" -eq 1 ]; then
491
523
  awk -v b="$BEGIN_MARK" -v e="$END_MARK" '$0 == b { keep = 1 } keep { print } $0 == e { keep = 0 }' "$f" >"$TMP_DIR/old"
492
524
  same "$TMP_DIR/old" "$TMP_DIR/block" && return 0
@@ -510,10 +542,13 @@ add_block() {
510
542
  }
511
543
 
512
544
  login_shell=$(basename "${SHELL:-sh}")
513
- # ~/.profile: read by sh and dash logins, by bash when it has no .bash_profile,
545
+ # ~/.profile: read by sh and dash logins, by bash when it has no .bash_profile or .bash_login,
514
546
  # and by the `sh -lc` Pantheon itself uses to find programs.
515
547
  add_block "$HOME/.profile"
516
548
  [ -f "$HOME/.bash_profile" ] && add_block "$HOME/.bash_profile"
549
+ # bash logins read the first of .bash_profile, .bash_login and .profile: with
550
+ # no .bash_profile, an existing .bash_login is the one read, not .profile.
551
+ [ -f "$HOME/.bash_login" ] && add_block "$HOME/.bash_login"
517
552
  { [ -f "$HOME/.bashrc" ] || [ "$login_shell" = bash ]; } && add_block "$HOME/.bashrc"
518
553
  { [ -f "$HOME/.zshrc" ] || [ "$login_shell" = zsh ]; } && add_block "$HOME/.zshrc"
519
554
  [ -f "$HOME/.zprofile" ] && add_block "$HOME/.zprofile"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.32.1",
3
+ "version": "0.33.0-rc.2",
4
4
  "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p .",
14
14
  "dev": "tsc -p . --watch",
15
15
  "relay": "node dist/cli.js relay",
16
- "test": "PANTHEON_HOME=\"$(mktemp -d)\" HOME=\"$(mktemp -d)\" node --test --test-force-exit dist/test/*.test.js",
16
+ "test": "PANTHEON_HOME=\"$(mktemp -d)\" HOME=\"$(mktemp -d)\" node --test --test-force-exit --import ./scripts/test-isolate.mjs dist/test/*.test.js",
17
17
  "prepare": "npm run build",
18
18
  "build:helpers": "node scripts/build-helpers.mjs",
19
19
  "release:check": "node scripts/release-check.mjs",