@join-pantheon/cli 0.32.1 → 0.33.0-rc.1

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:"
@@ -490,6 +670,16 @@ function Install-Pantheon {
490
670
  }
491
671
  } catch {
492
672
  $m = $_.Exception.Message
673
+ if ($m -like 'PANTHEON_BUSY:*') {
674
+ # Not a failure: the Pantheon already here keeps working. Only when there
675
+ # is none that runs is the exit code non-zero.
676
+ $have = ''
677
+ if ($installedCli) { try { $have = & $installedCli } catch { $have = '' } }
678
+ $kept = if ($have) { " Pantheon $have is still installed and working." } else { '' }
679
+ 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
680
+ $State.code = if ($have) { 0 } else { 1 }
681
+ return
682
+ }
493
683
  if ($m -like 'PANTHEON_FAIL:*') { $m = $m.Substring('PANTHEON_FAIL:'.Length) }
494
684
  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
685
  Write-Host "pantheon install failed: $m" -ForegroundColor Red
@@ -497,7 +687,33 @@ function Install-Pantheon {
497
687
  } finally {
498
688
  # Every way out, Ctrl-C included, lets go of the install lock.
499
689
  if ($State.lock) { $State.lock.Dispose(); $State.lock = $null }
690
+ # And starts again a background host this install stopped (on whichever Node is in place).
691
+ Start-PantheonTasks $State
692
+ }
693
+ }
694
+
695
+ # Start again the logon task Stop-PantheonHost stopped: this user's first task
696
+ # that starts. Once only. Its host removes the stop request when it starts.
697
+ function Start-PantheonTasks([hashtable]$State) {
698
+ if (-not $State.ContainsKey('restartTasks') -or $State.restarted) { return }
699
+ $State.restarted = $true
700
+ $schtasks = Join-Path $env:SystemRoot 'System32\schtasks.exe'
701
+ foreach ($t in $State.restartTasks) {
702
+ try {
703
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
704
+ $psi.FileName = $schtasks
705
+ $psi.Arguments = '/Run /TN "' + $t + '"'
706
+ $psi.UseShellExecute = $false
707
+ $psi.CreateNoWindow = $true
708
+ $psi.RedirectStandardOutput = $true
709
+ $psi.RedirectStandardError = $true
710
+ $p = [System.Diagnostics.Process]::Start($psi)
711
+ $null = $p.StandardOutput.ReadToEnd(); $null = $p.StandardError.ReadToEnd()
712
+ $p.WaitForExit()
713
+ if ($p.ExitCode -eq 0) { Write-Host "Started Pantheon's background host again ($t)."; return }
714
+ } catch { }
500
715
  }
716
+ 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
717
  }
502
718
 
503
719
  # `join '<link>'` as two arguments: whitespace splits, and single or double
@@ -548,7 +764,7 @@ $PantheonInstallCode = [int]$PantheonInstallState.code
548
764
  # result is left in $LASTEXITCODE instead.
549
765
  $PantheonOneShot = $false
550
766
  try { $PantheonOneShot = Test-PantheonOneShot ([Environment]::GetCommandLineArgs()) } catch { $PantheonOneShot = $false }
551
- Remove-Item Function:Install-Pantheon, Function:Split-PantheonArgs, Function:Test-PantheonOneShot -ErrorAction SilentlyContinue
767
+ Remove-Item Function:Install-Pantheon, Function:Start-PantheonTasks, Function:Split-PantheonArgs, Function:Test-PantheonOneShot -ErrorAction SilentlyContinue
552
768
  Remove-Variable PantheonInstallArgs, PantheonInstallState -ErrorAction SilentlyContinue
553
769
  $global:LASTEXITCODE = $PantheonInstallCode
554
770
  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"
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.1",
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",