@join-pantheon/cli 0.33.1 → 0.33.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.
Files changed (44) hide show
  1. package/README.md +6 -5
  2. package/dist/accounts.js +10 -0
  3. package/dist/agents.js +11 -0
  4. package/dist/board.js +55 -9
  5. package/dist/claude-confine.js +1 -1
  6. package/dist/cli.js +556 -89
  7. package/dist/client.js +8 -4
  8. package/dist/config.js +18 -4
  9. package/dist/continued.js +5 -0
  10. package/dist/desktop-app.js +103 -0
  11. package/dist/doctor.js +84 -32
  12. package/dist/folder-guard.js +12 -0
  13. package/dist/folder-snapshot.js +432 -0
  14. package/dist/install.js +15 -8
  15. package/dist/machine.js +84 -4
  16. package/dist/managed.js +2 -2
  17. package/dist/mcp.js +5 -3
  18. package/dist/network.js +156 -0
  19. package/dist/notice-linux.js +6 -5
  20. package/dist/notice-windows.js +2 -2
  21. package/dist/notice.js +13 -0
  22. package/dist/notifier.js +29 -2
  23. package/dist/notify.js +11 -0
  24. package/dist/onboard-defaults.js +37 -1
  25. package/dist/pins.js +7 -1
  26. package/dist/plans.js +23 -0
  27. package/dist/protocol.js +5 -0
  28. package/dist/quickstart.js +17 -11
  29. package/dist/refusal.js +4 -4
  30. package/dist/relay.js +332 -22
  31. package/dist/repo-find.js +6 -3
  32. package/dist/roster-file.js +50 -5
  33. package/dist/tray.js +12 -1
  34. package/dist/uninstall.js +38 -3
  35. package/dist/uri-handler.js +3 -2
  36. package/dist/verify-act.js +17 -0
  37. package/dist/worker-fleet.js +140 -5
  38. package/dist/worker.js +201 -63
  39. package/installer/install.ps1 +251 -15
  40. package/installer/install.sh +147 -18
  41. package/package.json +1 -1
  42. package/helper/macos/prebuilt/manifest.json +0 -29
  43. package/helper/macos/prebuilt/pantheon-hud +0 -0
  44. package/helper/macos/prebuilt/pantheon-tray +0 -0
@@ -190,6 +190,168 @@ function Install-Pantheon {
190
190
  return $mine
191
191
  }
192
192
 
193
+ # An app package's folder name as a person knows the app: Claude_pzs8sxrjxfjjc
194
+ # is the Claude desktop app; Publisher.Some.App_8wekyb3d8bbwe is Some.App.
195
+ function Get-PantheonAppName([string]$Package) {
196
+ if ($Package -match '^Claude_') { return 'the Claude desktop app' }
197
+ if ($Package) { return (($Package -replace '_[A-Za-z0-9]+$', '') -replace '^[A-Za-z0-9]+\.', '') }
198
+ return ''
199
+ }
200
+
201
+ # Copies of Pantheon that only an app could see: an installer before 0.33.1
202
+ # run from inside an app package (the Claude desktop app's agent) put
203
+ # Pantheon in %LOCALAPPDATA%\Pantheon, which Windows kept in that app's
204
+ # private AppData, %LOCALAPPDATA%\Packages\<package>\LocalCache\Local\Pantheon.
205
+ # Nothing outside the app ever ran it; it only takes space (npm's cache) and
206
+ # can confuse the app's agent. Pantheon's only by what the installer puts
207
+ # there (the launcher, the CLI, the install lock), never a folder another app
208
+ # happens to call Pantheon; and never through a link (junction or symlink) at
209
+ # any step of the path, which would lead somewhere else. Each as @{ path; package }.
210
+ function Find-PantheonAppCopies([string]$Local) {
211
+ $out = @()
212
+ if (-not $Local) { return , $out }
213
+ $pk = Join-Path $Local 'Packages'
214
+ if (-not (Test-Path -LiteralPath $pk -PathType Container)) { return , $out }
215
+ foreach ($f in @(Get-ChildItem -LiteralPath $pk -Directory -Force -ErrorAction SilentlyContinue)) {
216
+ $local2 = [IO.Path]::Combine($f.FullName, 'LocalCache', 'Local')
217
+ if (-not (Test-Path -LiteralPath $local2 -PathType Container)) { continue }
218
+ # The copy, and what a removal cut off halfway left beside it (named below, so ours by name).
219
+ $dirs = @()
220
+ $dir = Join-Path $local2 'Pantheon'
221
+ if (Test-Path -LiteralPath $dir -PathType Container) {
222
+ $ours = (Test-Path -LiteralPath ([IO.Path]::Combine($dir, 'bin', 'pantheon.cmd'))) -or
223
+ (Test-Path -LiteralPath ([IO.Path]::Combine($dir, 'runtime', 'node', 'node_modules', '@join-pantheon'))) -or
224
+ (Test-Path -LiteralPath ([IO.Path]::Combine($dir, 'runtime', 'install.lock')))
225
+ if ($ours) { $dirs += $dir }
226
+ }
227
+ # One suffix or more: a removal cut off twice by a build before 0.33.2 renamed it again.
228
+ $dirs += @(Get-ChildItem -LiteralPath $local2 -Directory -Force -ErrorAction SilentlyContinue |
229
+ Where-Object { $_.Name -match '^Pantheon(\.remove-[0-9a-f]{6})+$' } | ForEach-Object { $_.FullName })
230
+ foreach ($d in $dirs) {
231
+ $linked = $false
232
+ # Every step from the Packages folder down: a link at Packages itself, or
233
+ # at the package's folder, leads somewhere else just the same.
234
+ foreach ($step in @($pk, $f.FullName, [IO.Path]::Combine($f.FullName, 'LocalCache'), $local2, $d)) {
235
+ $item = $null
236
+ try { $item = Get-Item -LiteralPath $step -Force -ErrorAction Stop } catch { $item = $null }
237
+ if (-not $item -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { $linked = $true; break }
238
+ }
239
+ if (-not $linked) { $out += , @{ path = $d; package = $f.Name } }
240
+ }
241
+ }
242
+ return , $out
243
+ }
244
+
245
+ # A full path in the \\?\ form, which Windows takes past 260 characters.
246
+ function ConvertTo-PantheonLongPath([string]$Path) {
247
+ if ($Path.StartsWith('\\?\')) { return $Path }
248
+ if ($Path.StartsWith('\\')) { return '\\?\UNC\' + $Path.Substring(2) }
249
+ return '\\?\' + $Path
250
+ }
251
+
252
+ # rd /s /q by the \\?\ form of the path. Windows PowerShell 5.1's Remove-Item
253
+ # stops at 260 characters, and npm's cache inside such a copy goes deeper
254
+ # (removing one by hand took exactly this). The path is in double quotes,
255
+ # where cmd.exe reads & and ^ as they are. '' when the folder went; otherwise
256
+ # what rd said.
257
+ function Remove-PantheonTree([string]$Path) {
258
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
259
+ $psi.FileName = Join-Path $env:SystemRoot 'System32\cmd.exe'
260
+ $psi.Arguments = '/d /c rd /s /q "' + (ConvertTo-PantheonLongPath $Path) + '"'
261
+ $psi.UseShellExecute = $false
262
+ $psi.CreateNoWindow = $true
263
+ $psi.RedirectStandardInput = $true
264
+ $psi.RedirectStandardOutput = $true
265
+ $psi.RedirectStandardError = $true
266
+ $p = [System.Diagnostics.Process]::Start($psi)
267
+ $p.StandardInput.Close()
268
+ $out = $p.StandardOutput.ReadToEndAsync()
269
+ $err = $p.StandardError.ReadToEndAsync()
270
+ $p.WaitForExit()
271
+ if (-not (Test-Path -LiteralPath $Path)) { return '' }
272
+ $said = (($err.Result + ' ' + $out.Result) -replace '\s+', ' ').Trim()
273
+ if ($said) { return $said }
274
+ return "rd exited $($p.ExitCode)"
275
+ }
276
+
277
+ # Each copy nothing runs from is removed. First it is renamed beside itself
278
+ # ($Rename): Windows refuses to rename a folder a running program came from,
279
+ # however deep, which tells "in use" even where an app's private copy shows a
280
+ # running program under another path; and a copy cut off halfway is then no
281
+ # longer one anything starts. Then rd ($Remove: Remove-PantheonTree). A copy
282
+ # still in use is left as it is, with what runs from it ($Holders:
283
+ # Get-FolderHolders) when Windows says. Returns @{ removed = @(@{ path; package });
284
+ # held = @(@{ path; package; holders }); failed = @(@{ path; package; why }) }.
285
+ function Remove-PantheonAppCopies([object[]]$Copies, [scriptblock]$Holders, [scriptblock]$Remove,
286
+ [scriptblock]$Rename = { param($from, $to) Move-Item -LiteralPath $from -Destination $to -ErrorAction Stop }, [int]$Tries = 3) {
287
+ $removed = @(); $held = @(); $failed = @()
288
+ foreach ($c in @($Copies | Where-Object { $_ })) {
289
+ $h = @()
290
+ try { $h = @(& $Holders $c.path | ForEach-Object { $_ }) } catch { $h = @() }
291
+ # A copy already renamed aside (a removal cut off before) is renamed to a
292
+ # fresh name of the same form, never given a second suffix the next run
293
+ # would not know as Pantheon's.
294
+ $aside = ($c.path -replace '(\.remove-[0-9a-f]{6})+$', '') + '.remove-' + [Guid]::NewGuid().ToString('N').Substring(0, 6)
295
+ $moved = $false
296
+ if ($h.Count -eq 0) {
297
+ for ($i = 1; $i -le $Tries -and -not $moved; $i++) {
298
+ try { & $Rename $c.path $aside; $moved = $true } catch { if ($i -lt $Tries) { Start-Sleep -Milliseconds 700 } }
299
+ }
300
+ }
301
+ if (-not $moved) { $held += , @{ path = $c.path; package = $c.package; holders = $h }; continue }
302
+ $why = ''
303
+ try { $why = [string](& $Remove $aside) } catch { $why = $_.Exception.Message }
304
+ if (Test-Path -LiteralPath $aside) {
305
+ if (-not $why) { $why = 'some of it is still there' }
306
+ $failed += , @{ path = $aside; package = $c.package; why = $why }
307
+ } else { $removed += , @{ path = $c.path; package = $c.package } }
308
+ }
309
+ return @{ removed = $removed; held = $held; failed = $failed }
310
+ }
311
+
312
+ # What a shell is, from its program's name: the syntax its next command
313
+ # is said in. '' when it cannot be told.
314
+ function Get-PantheonShellKind([string]$Name) {
315
+ $n = ([string]$Name).ToLowerInvariant() -replace '\.exe$', ''
316
+ if ($n -eq 'cmd') { return 'cmd' }
317
+ if ($n -in @('powershell', 'pwsh', 'powershell_ise')) { return 'powershell' }
318
+ if ($n -in @('bash', 'sh', 'zsh', 'dash', 'fish', 'mintty', 'git-bash')) { return 'bash' }
319
+ return ''
320
+ }
321
+
322
+ # The program that started this PowerShell (cmd.exe, when the installer
323
+ # was run as powershell -Command "..." from Command Prompt). '' when
324
+ # Windows will not say.
325
+ function Get-PantheonParentName {
326
+ try {
327
+ $me = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $PID) -ErrorAction Stop
328
+ $up = Get-CimInstance Win32_Process -Filter ('ProcessId=' + [int]$me.ParentProcessId) -ErrorAction Stop
329
+ return [string]$up.Name
330
+ } catch { return '' }
331
+ }
332
+
333
+ # A pantheon command line as typed in $Shell: by name ($Launcher '') where
334
+ # pantheon is already on that window's PATH, otherwise by the launcher's
335
+ # full path in that shell's own syntax. Arguments are quoted only where
336
+ # that shell needs it, so the line reads as typed. A bash on Windows (Git
337
+ # Bash) runs a .cmd only by its full name, and reads C:/ paths, so it gets
338
+ # one of those.
339
+ function Format-PantheonCommand([string]$Launcher, [string[]]$Arguments, [string]$Shell) {
340
+ $plain = '^[A-Za-z0-9_./:=@+,-]+$'
341
+ $items = @($Arguments | Where-Object { $null -ne $_ })
342
+ if ($Shell -eq 'cmd') {
343
+ $head = if ($Launcher) { '"' + $Launcher + '"' } else { 'pantheon' }
344
+ $rest = @($items | ForEach-Object { if ($_ -match $plain) { $_ } else { '"' + $_ + '"' } })
345
+ } elseif ($Shell -eq 'bash') {
346
+ $head = if ($Launcher) { "'" + (($Launcher -replace '\\', '/') -replace "'", "'\''") + "'" } else { 'pantheon' }
347
+ $rest = @($items | ForEach-Object { if ($_ -match $plain) { $_ } else { "'" + ($_ -replace "'", "'\''") + "'" } })
348
+ } else {
349
+ $head = if ($Launcher) { "& '" + ($Launcher -replace "'", "''") + "'" } else { 'pantheon' }
350
+ $rest = @($items | ForEach-Object { if ($_ -match $plain) { $_ } else { "'" + ($_ -replace "'", "''") + "'" } })
351
+ }
352
+ return ((@($head) + @($rest)) -join ' ')
353
+ }
354
+
193
355
  # The user PATH, set from OUTSIDE this app's package. Inside one, Windows 10
194
356
  # keeps HKCU changes private to the app (Windows 11 lets this key through),
195
357
  # so a terminal opened from the Start menu would not find pantheon. Task
@@ -390,7 +552,7 @@ function Install-Pantheon {
390
552
  param([string]$NodeDir, [string]$HostDir, [string[]]$Tasks, [scriptblock]$Holders, [scriptblock]$EndTask, [int]$WaitSeconds = 15,
391
553
  [string]$RunRecord = '', [scriptblock]$RunAlive = { param($p) $null -ne (Get-Process -Id $p -ErrorAction SilentlyContinue) },
392
554
  [int]$RunWaitSeconds = 180, [scriptblock]$TaskIsMine = { param($t) $true }, [scriptblock]$Say = { param($m) Write-Host $m })
393
- $left = @(& $Holders $NodeDir)
555
+ $left = @(& $Holders $NodeDir | ForEach-Object { $_ })
394
556
  if ($left.Count -eq 0) { return @{ restart = $false; holders = @(); busy = $false; tasks = @() } }
395
557
  if (Get-PantheonRunInFlight $RunRecord $RunAlive) {
396
558
  & $Say "A teammate's request is running here; waiting for it to finish before updating (up to $([math]::Ceiling($RunWaitSeconds / 60)) minutes)..."
@@ -401,7 +563,7 @@ function Install-Pantheon {
401
563
  $running = [bool](Get-PantheonRunInFlight $RunRecord $RunAlive)
402
564
  }
403
565
  if ($running) { return @{ restart = $false; holders = $left; busy = $true; tasks = @() } }
404
- $left = @(& $Holders $NodeDir)
566
+ $left = @(& $Holders $NodeDir | ForEach-Object { $_ })
405
567
  if ($left.Count -eq 0) { return @{ restart = $false; holders = @(); busy = $false; tasks = @() } }
406
568
  }
407
569
  $hostUp = Test-Path -LiteralPath (Join-Path $HostDir 'state.json')
@@ -411,12 +573,12 @@ function Install-Pantheon {
411
573
  $deadline = (Get-Date).AddSeconds($WaitSeconds)
412
574
  while ($hostUp -and $left.Count -gt 0 -and (Get-Date) -lt $deadline) {
413
575
  Start-Sleep -Milliseconds 500
414
- $left = @(& $Holders $NodeDir)
576
+ $left = @(& $Holders $NodeDir | ForEach-Object { $_ })
415
577
  }
416
578
  $mine = @($Tasks | Where-Object { $_ } | Where-Object { $ok = $false; try { $ok = [bool](& $TaskIsMine $_) } catch { $ok = $false }; $ok })
417
579
  if ($left.Count -gt 0) {
418
580
  foreach ($t in $mine) { & $EndTask $t }
419
- for ($i = 0; $i -lt 6 -and $left.Count -gt 0; $i++) { Start-Sleep -Milliseconds 500; $left = @(& $Holders $NodeDir) }
581
+ for ($i = 0; $i -lt 6 -and $left.Count -gt 0; $i++) { Start-Sleep -Milliseconds 500; $left = @(& $Holders $NodeDir | ForEach-Object { $_ }) }
420
582
  }
421
583
  return @{ restart = $hostUp; holders = $left; busy = $false; tasks = $mine }
422
584
  }
@@ -489,11 +651,13 @@ function Install-Pantheon {
489
651
  $signedIn = $false
490
652
  try { $signedIn = [bool]((Get-Content -LiteralPath (Join-Path $pantheonHome 'config.json') -Raw | ConvertFrom-Json).account) } catch { $signedIn = $false }
491
653
  if (-not $signedIn) {
492
- # Quoted only where PowerShell needs it, so it reads as typed.
493
- $show = ($Arguments | ForEach-Object { if ($_ -match '^[A-Za-z0-9_./:=@+,-]+$') { $_ } else { "'" + ($_ -replace "'", "''") + "'" } }) -join ' '
654
+ # By the launcher's full path, as typed in PowerShell: whatever runs it
655
+ # next (an agent's shell, a window opened before this install) may not
656
+ # have the new PATH yet.
657
+ $show = Format-PantheonCommand $launcher $Arguments 'powershell'
494
658
  $what = if ($Arguments[0] -eq 'join') { 'joining' } else { 'creating your team' }
495
659
  Say ''
496
- Say "Pantheon is installed. To finish $what, open PowerShell and run: pantheon $show"
660
+ Say "Pantheon is installed. To finish $what, open PowerShell and run: $show"
497
661
  }
498
662
  }
499
663
  } finally {
@@ -549,7 +713,7 @@ function Install-Pantheon {
549
713
  if (-not $profileDir) { Fail 'USERPROFILE is not set, so there is nowhere to install. Run this from your own Windows account.' }
550
714
  # Inside an app package (the Claude desktop app): said once, and acted on below.
551
715
  $appPackage = Get-PantheonAppPackage $localAppData
552
- $appName = if ($appPackage -match '^Claude_') { 'the Claude desktop app' } elseif ($appPackage) { (($appPackage -replace '_[A-Za-z0-9]+$', '') -replace '^[A-Za-z0-9]+\.', '') } else { '' }
716
+ $appName = Get-PantheonAppName $appPackage
553
717
  $where = Select-PantheonRoot $profileDir $localAppData $appPackage
554
718
  $root = $where.root
555
719
  # A release before 0.33.1 does not know ~\.pantheon (it would not find itself to
@@ -573,6 +737,38 @@ function Install-Pantheon {
573
737
  $binDir = Join-Path $root 'bin'
574
738
  $launcher = Join-Path $binDir 'pantheon.cmd'
575
739
  $log = Join-Path $root 'install.log'
740
+
741
+ # The window the person ran this from, and how pantheon is typed there
742
+ # once this is done. An open PowerShell window running irm | iex is this
743
+ # very process, whose PATH is set below: pantheon by name. Run as
744
+ # powershell -Command "..." from another window (Command Prompt, a
745
+ # PowerShell window, Git Bash, an agent's shell), that window keeps the PATH
746
+ # it started with, so pantheon is "not recognized" there: by name only if
747
+ # pantheon already meant this launcher in it (asked now, before this
748
+ # script changes PATH), otherwise by its full path in that window's syntax.
749
+ $oneShot = $false
750
+ try { $oneShot = Test-PantheonOneShot ([Environment]::GetCommandLineArgs()) -OrFile } catch { $oneShot = $false }
751
+ $shellKind = 'here'
752
+ $byPath = ''
753
+ if ($oneShot) {
754
+ $shellKind = Get-PantheonShellKind (Get-PantheonParentName)
755
+ $known = $null
756
+ try { $known = Get-Command pantheon -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 } catch { $known = $null }
757
+ if ($shellKind -eq 'bash' -or -not ($known -and $known.Source -ieq $launcher)) { $byPath = $launcher }
758
+ }
759
+ # The next command for that window. Where the shell cannot be told, in
760
+ # PowerShell's syntax, and the note below gives Command Prompt's.
761
+ $nextFor = {
762
+ param([string[]]$Items)
763
+ if ($shellKind -eq 'here') { return Format-PantheonCommand '' $Items 'powershell' }
764
+ if ($shellKind) { return Format-PantheonCommand $byPath $Items $shellKind }
765
+ return Format-PantheonCommand $byPath $Items 'powershell'
766
+ }
767
+ $byPathNote = {
768
+ if (-not $byPath) { return }
769
+ $cmdToo = if ($shellKind) { '' } else { " (in Command Prompt: $(Format-PantheonCommand $byPath @() 'cmd'))" }
770
+ Say "(This window was open before Pantheon was on your PATH, so that is its full path$cmdToo; a new window finds it as pantheon.)"
771
+ }
576
772
  $pantheonHomeDir = if ($env:PANTHEON_HOME) { $env:PANTHEON_HOME } else { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.pantheon' }
577
773
  $hostDir = Join-Path $pantheonHomeDir 'host'
578
774
 
@@ -867,6 +1063,28 @@ function Install-Pantheon {
867
1063
  Where-Object { $_.Source -and -not $_.Source.StartsWith($root + '\', [StringComparison]::OrdinalIgnoreCase) } | Select-Object -First 1
868
1064
  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." }
869
1065
 
1066
+ # ---- old copies that only an app could see -------------------------------
1067
+
1068
+ # Only from outside an app package: inside one, its private copy is what
1069
+ # this very app may be running, and Select-PantheonRoot already passes it over.
1070
+ if (-not $appPackage) {
1071
+ $State.step = 'removing old copies of Pantheon that only an app could see'
1072
+ $copies = Find-PantheonAppCopies $localAppData
1073
+ if ($copies.Count) {
1074
+ $gone = Remove-PantheonAppCopies $copies { param($d) Get-FolderHolders $d } { param($d) Remove-PantheonTree $d }
1075
+ foreach ($c in $gone.removed) { Say "Removed an old copy of Pantheon that only $(Get-PantheonAppName $c.package) could see: $($c.path)" }
1076
+ foreach ($c in $gone.held) {
1077
+ $n = Get-PantheonAppName $c.package
1078
+ $what = if (@($c.holders).Count) { Format-Holders $c.holders } else { "a program (Windows did not say which; most likely $n)" }
1079
+ Note "an old copy of Pantheon that only $n could see is in $($c.path), and $what runs from it. Quit $n, then run this installer again to remove it."
1080
+ }
1081
+ foreach ($c in $gone.failed) {
1082
+ $n = Get-PantheonAppName $c.package
1083
+ Note "an old copy of Pantheon that only $n could see was only partly removed: $($c.path) ($($c.why)). Quit $n, then run this installer again to finish."
1084
+ }
1085
+ }
1086
+ }
1087
+
870
1088
  # ---- 5. hand over to pantheon ------------------------------------------
871
1089
 
872
1090
  $State.lock.Dispose()
@@ -919,17 +1137,26 @@ function Install-Pantheon {
919
1137
  Say ''
920
1138
  if ($member) {
921
1139
  Say "Pantheon $PantheonVersion is installed, and you are still signed in as $member -- nothing to rejoin."
922
- Say 'Open a new terminal (or restart your agent) and carry on; pantheon doctor checks everything.'
1140
+ Say "Restart any agent window that was open and carry on. To check everything, in this window: $(& $nextFor @('doctor'))"
923
1141
  } else {
924
- Say "Pantheon $PantheonVersion is installed. Open a new terminal, then:"
925
- Say ' pantheon start create a team'
926
- Say " pantheon join '<invite link>' join a teammate's team"
1142
+ Say "Pantheon $PantheonVersion is installed. Next, in this window:"
1143
+ $startLine = & $nextFor @('start')
1144
+ $joinLine = & $nextFor @('join', '<invite link>')
1145
+ # The start line padded to the join line's width, so the descriptions line up.
1146
+ Say (' ' + $startLine.PadRight($joinLine.Length) + ' create a team')
1147
+ Say (' ' + $joinLine + " join a teammate's team")
927
1148
  }
1149
+ & $byPathNote
928
1150
  $State.code = 0
929
1151
  return
930
1152
  }
931
1153
  Say ''
932
1154
  Invoke-PantheonHandoff $nodeExe $entry $ca $Arguments
1155
+ if ($State.code -eq 0) {
1156
+ Say ''
1157
+ Say "Next, in this window: $(& $nextFor @('doctor')) (checks that everything works)"
1158
+ & $byPathNote
1159
+ }
933
1160
  } catch {
934
1161
  $m = $_.Exception.Message
935
1162
  if ($m -like 'PANTHEON_BUSY:*') {
@@ -947,6 +1174,11 @@ function Install-Pantheon {
947
1174
  try {
948
1175
  Say ''
949
1176
  Invoke-PantheonHandoff $nodeExe $entry $caNow $Arguments
1177
+ if ($State.code -eq 0 -and $nextFor) {
1178
+ Say ''
1179
+ Say "Next, in this window: $(& $nextFor @('doctor')) (checks that everything works)"
1180
+ & $byPathNote
1181
+ }
950
1182
  } catch {
951
1183
  Write-Host "pantheon install failed: it stopped while starting pantheon ($($_.Exception.Message)). Open a new terminal and run your pantheon command again." -ForegroundColor Red
952
1184
  $State.code = 1
@@ -1016,15 +1248,19 @@ function Split-PantheonArgs([string]$Line) {
1016
1248
  # (`powershell -Command "... | iex"` from cmd.exe), where the exit code is the
1017
1249
  # result, rather than an open window, which `exit` would close. PowerShell
1018
1250
  # takes any prefix of a parameter's name, after -, -- or /; -NoExit anywhere
1019
- # keeps the window open, so then it is never one-shot.
1020
- function Test-PantheonOneShot([string[]]$CommandLine) {
1251
+ # keeps the window open, so then it is never one-shot. -OrFile also counts a
1252
+ # script run with -File (or -f): the window it was started from keeps its own
1253
+ # PATH, so the next command names the launcher by path there. Only for that
1254
+ # line: the script may be someone's own that runs this installer and goes on,
1255
+ # which `exit` would end.
1256
+ function Test-PantheonOneShot([string[]]$CommandLine, [switch]$OrFile) {
1021
1257
  $flags = @($CommandLine | Select-Object -Skip 1 | Where-Object { $_ -match '^(--?|/)[A-Za-z]+$' } |
1022
1258
  ForEach-Object { ($_ -replace '^(--?|/)', '').ToLowerInvariant() })
1023
1259
  $prefixOf = { param($f, $name, $min) $f.Length -ge $min -and $name.StartsWith($f) }
1024
1260
  if ($flags | Where-Object { & $prefixOf $_ 'noexit' 3 }) { return $false }
1025
1261
  foreach ($f in $flags) {
1026
1262
  if ((& $prefixOf $f 'command' 1) -or (& $prefixOf $f 'commandwithargs' 8) -or $f -eq 'cwa' -or
1027
- (& $prefixOf $f 'encodedcommand' 1) -or $f -eq 'ec') { return $true }
1263
+ (& $prefixOf $f 'encodedcommand' 1) -or $f -eq 'ec' -or ($OrFile -and (& $prefixOf $f 'file' 1))) { return $true }
1028
1264
  }
1029
1265
  return $false
1030
1266
  }
@@ -541,7 +541,81 @@ add_block() {
541
541
  added="$added ${f#"$HOME"/}"
542
542
  }
543
543
 
544
- login_shell=$(basename "${SHELL:-sh}")
544
+ # The shell this account logs in with: what a new terminal starts. $SHELL is
545
+ # only the shell of whatever ran this installer, and an app or an agent passes
546
+ # its own (/bin/sh) or none: on 6 Oct a new Mac (no ~/.zshrc yet) installed
547
+ # from an agent got no zsh block, and a new Terminal said "pantheon: command
548
+ # not found". So the account's own record first: macOS's directory (dscl),
549
+ # Linux's passwd (getent, else /etc/passwd); $SHELL only when neither says.
550
+ account_shell() {
551
+ me=$(id -un 2>/dev/null || printf '%s' "${USER:-}")
552
+ s=""
553
+ if [ -n "$me" ]; then
554
+ if [ "$node_os" = darwin ]; then
555
+ s=$(dscl . -read "/Users/$me" UserShell 2>/dev/null | sed -n 's/^UserShell:[[:space:]]*//p' | head -n 1 || true)
556
+ else
557
+ s=$(getent passwd "$me" 2>/dev/null | cut -d: -f7 || true)
558
+ [ -n "$s" ] || s=$(awk -F: -v u="$me" '$1 == u { print $7; exit }' /etc/passwd 2>/dev/null || true)
559
+ fi
560
+ fi
561
+ [ -n "$s" ] || s=${SHELL:-}
562
+ basename "${s:-sh}"
563
+ }
564
+ login_shell=$(account_shell)
565
+ # zsh reads .zshrc and .zprofile from $ZDOTDIR when that is set (usually in
566
+ # ~/.zshenv), not from HOME: asked of zsh itself, which reads ~/.zshenv as
567
+ # every new zsh does. After a marker, so whatever .zshenv prints is not taken.
568
+ # A .zshenv can wait for input (from the terminal, past a stdin of /dev/null)
569
+ # or block outright, so zsh is asked in the background, into a file (a pipe a
570
+ # leftover child kept open would wait with it), and killed after 5 s; then the
571
+ # answer is this environment's $ZDOTDIR, else HOME. POSIX only: macOS has no
572
+ # `timeout`. The watchdog's own output goes nowhere, so nothing waits on it.
573
+ zdot=$HOME
574
+ z=${ZDOTDIR:-}
575
+ if command -v zsh >/dev/null 2>&1; then
576
+ : >"$TMP_DIR/zdotdir"
577
+ zsh -c 'printf "\n__pantheon_zdotdir__%s\n" "${ZDOTDIR:-}"' </dev/null >"$TMP_DIR/zdotdir" 2>/dev/null &
578
+ zsh_pid=$!
579
+ (sleep 5; : >"$TMP_DIR/zsh-slow"; kill -9 "$zsh_pid") </dev/null >/dev/null 2>&1 &
580
+ watch_pid=$!
581
+ wait "$zsh_pid" 2>/dev/null || true
582
+ kill "$watch_pid" 2>/dev/null || true
583
+ if grep -q '^__pantheon_zdotdir__' "$TMP_DIR/zdotdir" 2>/dev/null; then
584
+ z=$(sed -n 's/^__pantheon_zdotdir__//p' "$TMP_DIR/zdotdir" | tail -n 1)
585
+ elif [ -f "$TMP_DIR/zsh-slow" ]; then
586
+ note "zsh did not start within 5 seconds (something in ~/.zshenv waits), so its folder was taken from \$ZDOTDIR here, or the home folder"
587
+ fi
588
+ fi
589
+ # A folder's real path (cd ... && pwd -P); one not made yet is its nearest existing
590
+ # parent's real path and the rest, which must then hold no . or .. to be judged.
591
+ real_dir() {
592
+ rd=$1
593
+ rest=""
594
+ while [ -n "$rd" ] && [ "$rd" != / ] && [ ! -d "$rd" ]; do
595
+ rest="/$(basename "$rd")$rest"
596
+ rd=$(dirname "$rd")
597
+ done
598
+ case "$rest" in */. | */./* | */.. | */../*) return 1 ;; esac
599
+ rd=$(cd "$rd" 2>/dev/null && pwd -P) || return 1
600
+ printf '%s%s' "${rd%/}" "$rest"
601
+ }
602
+ # Only a folder inside HOME, by its real path: $HOME/../bob, or a link in HOME to
603
+ # a folder elsewhere, is outside, and nothing is written outside the home folder.
604
+ # Named under HOME as given, so the list of files changed reads as the person knows it.
605
+ z=${z%/}
606
+ if [ -n "$z" ] && [ "$z" != "$HOME" ]; then
607
+ zr=$(real_dir "$z" || true)
608
+ hr=$(real_dir "$HOME" || true)
609
+ [ -n "$hr" ] || hr=$HOME
610
+ case "$zr" in
611
+ "$hr"/?*) zdot="$HOME${zr#"$hr"}" ;;
612
+ *)
613
+ if [ "$zr" != "$hr" ]; then
614
+ note "zsh reads its startup files from $z (ZDOTDIR), outside your home folder, so nothing was written there; to have pantheon in new zsh terminals, put $BIN_DIR at the front of PATH in $z/.zshrc yourself"
615
+ fi
616
+ ;;
617
+ esac
618
+ fi
545
619
  # ~/.profile: read by sh and dash logins, by bash when it has no .bash_profile or .bash_login,
546
620
  # and by the `sh -lc` Pantheon itself uses to find programs.
547
621
  add_block "$HOME/.profile"
@@ -549,11 +623,23 @@ add_block "$HOME/.profile"
549
623
  # bash logins read the first of .bash_profile, .bash_login and .profile: with
550
624
  # no .bash_profile, an existing .bash_login is the one read, not .profile.
551
625
  [ -f "$HOME/.bash_login" ] && add_block "$HOME/.bash_login"
626
+ # An interactive bash that is not a login (most Linux terminals) reads only .bashrc.
552
627
  { [ -f "$HOME/.bashrc" ] || [ "$login_shell" = bash ]; } && add_block "$HOME/.bashrc"
553
- { [ -f "$HOME/.zshrc" ] || [ "$login_shell" = zsh ]; } && add_block "$HOME/.zshrc"
554
- [ -f "$HOME/.zprofile" ] && add_block "$HOME/.zprofile"
628
+ # zsh, login or not, reads .zshrc. A Mac's terminals start zsh unless the
629
+ # person chose otherwise, and a new Mac has no .zshrc: there it is always
630
+ # written (made when missing), whatever this installer was run from.
631
+ if [ -f "$zdot/.zshrc" ] || [ "$login_shell" = zsh ] || [ "$node_os" = darwin ]; then
632
+ mkdir -p "$zdot" 2>/dev/null || true
633
+ add_block "$zdot/.zshrc"
634
+ fi
635
+ [ -f "$zdot/.zprofile" ] && add_block "$zdot/.zprofile"
636
+ # With ZDOTDIR elsewhere, a block already in HOME's own zsh files is kept current.
637
+ if [ "$zdot" != "$HOME" ]; then
638
+ [ -f "$HOME/.zshrc" ] && add_block "$HOME/.zshrc"
639
+ [ -f "$HOME/.zprofile" ] && add_block "$HOME/.zprofile"
640
+ fi
555
641
  # fish reads its own conf.d folder; the file there is ours whole.
556
- if command -v fish >/dev/null 2>&1 || [ -d "$HOME/.config/fish" ]; then
642
+ if command -v fish >/dev/null 2>&1 || [ -d "$HOME/.config/fish" ] || [ "$login_shell" = fish ]; then
557
643
  fish_conf="$HOME/.config/fish/conf.d/pantheon.fish"
558
644
  # shellcheck disable=SC2016 # written literally: fish expands it, not this script
559
645
  {
@@ -577,6 +663,28 @@ if [ -n "$other" ] && [ "$other" != "$LAUNCHER" ]; then
577
663
  note "another pantheon is installed at $other and was left as it is. New terminals run $LAUNCHER first."
578
664
  fi
579
665
 
666
+ # The next command, for the terminal this was run from. That terminal read its
667
+ # PATH before the profiles above changed, so unless pantheon already meant this
668
+ # launcher there, the line names it by its full path; a new terminal finds it
669
+ # by name. Quoted only where a shell needs it, so it reads as typed.
670
+ shown() {
671
+ case "$1" in
672
+ "" | *[!A-Za-z0-9_./:=@%+,-]*) shq "$1" ;;
673
+ *) printf '%s' "$1" ;;
674
+ esac
675
+ }
676
+ if [ "$other" = "$LAUNCHER" ]; then PANTHEON_HERE="pantheon"; else PANTHEON_HERE=$(shown "$LAUNCHER"); fi
677
+ next_line() {
678
+ line="$PANTHEON_HERE"
679
+ for a in "$@"; do line="$line $(shown "$a")"; done
680
+ printf '%s' "$line"
681
+ }
682
+ by_path_note() {
683
+ if [ "$PANTHEON_HERE" != "pantheon" ]; then
684
+ say "(This terminal was open before Pantheon was on your PATH, so that is its full path; a new terminal finds it as pantheon.)"
685
+ fi
686
+ }
687
+
580
688
  # ---- 5. hand over to pantheon ----------------------------------------------
581
689
 
582
690
  rm -rf "$TMP_DIR"
@@ -586,9 +694,14 @@ trap - EXIT INT TERM HUP
586
694
 
587
695
  if [ "$#" -eq 0 ]; then
588
696
  say ""
589
- say "Pantheon $PANTHEON_VERSION is installed. Open a new terminal (or run: export PATH=\"$BIN_DIR:\$PATH\"), then:"
590
- say " pantheon start create a team"
591
- say " pantheon join '<invite link>' join a teammate's team"
697
+ say "Pantheon $PANTHEON_VERSION is installed. Next, in this terminal:"
698
+ start_line=$(next_line start)
699
+ join_line="$PANTHEON_HERE join '<invite link>'"
700
+ # The start line padded to the join line's width, so the descriptions line up.
701
+ while [ "${#start_line}" -lt "${#join_line}" ]; do start_line="$start_line "; done
702
+ say " $start_line create a team"
703
+ say " $join_line join a teammate's team"
704
+ by_path_note
592
705
  exit 0
593
706
  fi
594
707
 
@@ -596,12 +709,32 @@ say ""
596
709
  # Found first by anything pantheon starts during setup, before any new terminal.
597
710
  PATH="$BIN_DIR:$PATH"
598
711
  export PATH
712
+ # Once pantheon has done what was asked, the last line is the next command for
713
+ # this terminal. Not exec, so there is a last line; Ctrl-C still reaches
714
+ # pantheon (a trap is reset in a child, where an ignored signal would not be)
715
+ # and this shell waits for it rather than dying first.
716
+ finish() {
717
+ if [ "$1" -eq 0 ]; then
718
+ say ""
719
+ say "Next, in this terminal: $(next_line doctor) (checks that everything works)"
720
+ by_path_note
721
+ fi
722
+ exit "$1"
723
+ }
599
724
  # stdin is the curl pipe; sign-in and setup questions must come from the
600
725
  # keyboard.
601
726
  if (: </dev/tty) 2>/dev/null; then
602
- exec "$LAUNCHER" "$@" </dev/tty
727
+ trap ':' INT
728
+ code=0
729
+ "$LAUNCHER" "$@" </dev/tty || code=$?
730
+ finish "$code"
731
+ fi
732
+ if [ -t 0 ]; then
733
+ trap ':' INT
734
+ code=0
735
+ "$LAUNCHER" "$@" || code=$?
736
+ finish "$code"
603
737
  fi
604
- if [ -t 0 ]; then exec "$LAUNCHER" "$@"; fi
605
738
 
606
739
  # No terminal at all: CI, a pipe, some IDE task runners. pantheon still runs,
607
740
  # because it never asks a question without one (every prompt checks
@@ -629,14 +762,10 @@ if [ "$code" -ne 0 ] && [ -n "$said" ] && grep -qE 'sign in first|NOT_SIGNED_IN'
629
762
  join | start)
630
763
  cfg="${PANTHEON_HOME:-$HOME/.pantheon}/config.json"
631
764
  if ! "$NODE" -e 'try { process.exit(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).account ? 0 : 1) } catch { process.exit(1) }' "$cfg" 2>/dev/null; then
632
- line="pantheon"
633
- # Quoted only where a shell needs it, so it reads as the person typed it.
634
- for a in "$@"; do
635
- case "$a" in
636
- "" | *[!A-Za-z0-9_./:=@%+,-]*) line="$line $(shq "$a")" ;;
637
- *) line="$line $a" ;;
638
- esac
639
- done
765
+ # By its full path: whatever runs it next (an agent's shell, a terminal
766
+ # opened before this install) may not have the new PATH yet.
767
+ line=$(shown "$LAUNCHER")
768
+ for a in "$@"; do line="$line $(shown "$a")"; done
640
769
  if [ "$1" = join ]; then what="joining"; else what="creating your team"; fi
641
770
  printf '\nPantheon is installed. To finish %s, open a terminal and run: %s\n' "$what" "$line" >&2
642
771
  fi
@@ -644,4 +773,4 @@ if [ "$code" -ne 0 ] && [ -n "$said" ] && grep -qE 'sign in first|NOT_SIGNED_IN'
644
773
  esac
645
774
  fi
646
775
  if [ -n "$said" ]; then rm -f "$said" "$said.code"; fi
647
- exit "$code"
776
+ finish "$code"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.33.1",
3
+ "version": "0.33.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": {
@@ -1,29 +0,0 @@
1
- {
2
- "format": 1,
3
- "built_at": "2026-10-05T17:23:08.044Z",
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": "191f3f18e8212e11ad4438bdbef87a5cc533f5ae2fae208801db01c9a265193b",
21
- "archs": [
22
- "arm64",
23
- "x86_64"
24
- ],
25
- "target": "macos11",
26
- "version": "4"
27
- }
28
- }
29
- }
Binary file
Binary file