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

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.
@@ -186,8 +186,11 @@ function Install-Pantheon {
186
186
  # Is a task this user's? Its XML (schtasks /Query /TN <name> /XML) names its
187
187
  # principal as DOMAIN\user, a bare user name, or a SID; every one of them must
188
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) {
189
+ # another person on this PC registered is never ended. A SID is compared as a
190
+ # SID; a name is first turned into its account's SID ($ToSid) and compared
191
+ # by that, and only by name when Windows cannot say which account it is.
192
+ function Test-PantheonTaskIsMine([string]$Xml, [string]$UserId, [string]$Sid,
193
+ [scriptblock]$ToSid = { param($n) if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { return '' }; try { ([Security.Principal.NTAccount]$n).Translate([Security.Principal.SecurityIdentifier]).Value } catch { '' } }) {
191
194
  if (-not $Xml) { return $false }
192
195
  $ids = @([regex]::Matches(($Xml -replace "`0", ''), '<UserId>([^<]*)</UserId>') | ForEach-Object { $_.Groups[1].Value.Trim().ToLowerInvariant() })
193
196
  if ($ids.Count -eq 0) { return $false }
@@ -195,14 +198,57 @@ function Install-Pantheon {
195
198
  $s = ([string]$Sid).ToLowerInvariant()
196
199
  $bare = if ($u.Contains('\')) { $u.Substring($u.LastIndexOf('\') + 1) } else { $u }
197
200
  foreach ($id in $ids) {
201
+ if ($id.StartsWith('s-1-')) { if ($s -and $id -eq $s) { continue }; return $false }
202
+ $idSid = ''
203
+ if ($s) { try { $idSid = ([string](& $ToSid $id)).ToLowerInvariant() } catch { $idSid = '' } }
204
+ if ($idSid) { if ($idSid -eq $s) { continue }; return $false }
198
205
  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 }
206
+ if ($bare -and -not $id.Contains('\') -and $id -eq $bare) { continue }
201
207
  return $false
202
208
  }
203
209
  return $true
204
210
  }
205
211
 
212
+ # What schtasks printed, as text. It writes a task's XML as UTF-16 (with or
213
+ # without a byte order mark); read in the console's code page, a user name
214
+ # with a letter outside ASCII came out as other letters and never matched.
215
+ # So the bytes are decoded by what they hold.
216
+ function ConvertFrom-TaskXmlBytes([byte[]]$Bytes) {
217
+ if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
218
+ if ($Bytes.Length -ge 2 -and $Bytes[0] -eq 0xFF -and $Bytes[1] -eq 0xFE) { return [Text.Encoding]::Unicode.GetString($Bytes, 2, $Bytes.Length - 2) }
219
+ if ($Bytes.Length -ge 2 -and $Bytes[0] -eq 0xFE -and $Bytes[1] -eq 0xFF) { return [Text.Encoding]::BigEndianUnicode.GetString($Bytes, 2, $Bytes.Length - 2) }
220
+ if ($Bytes.Length -ge 3 -and $Bytes[0] -eq 0xEF -and $Bytes[1] -eq 0xBB -and $Bytes[2] -eq 0xBF) { return [Text.Encoding]::UTF8.GetString($Bytes, 3, $Bytes.Length - 3) }
221
+ # No mark: UTF-16LE when most odd bytes of the start are zero ('<', 0, '?', 0, ...).
222
+ $n = [Math]::Min($Bytes.Length, 64); $odd = 0; $zeros = 0
223
+ for ($i = 1; $i -lt $n; $i += 2) { $odd++; if ($Bytes[$i] -eq 0) { $zeros++ } }
224
+ if ($odd -gt 0 -and $zeros * 4 -ge $odd * 3) { return [Text.Encoding]::Unicode.GetString($Bytes) }
225
+ try { return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($Bytes) } catch { }
226
+ return [Console]::OutputEncoding.GetString($Bytes)
227
+ }
228
+
229
+ # A task's XML (schtasks /Query /TN <name> /XML), its bytes read as they are
230
+ # (ConvertFrom-TaskXmlBytes); '' when there is no such task.
231
+ function Get-PantheonTaskXml([string]$Schtasks, [string]$Name) {
232
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
233
+ $psi.FileName = $Schtasks
234
+ $psi.Arguments = ConvertTo-ArgLine @('/Query', '/TN', $Name, '/XML')
235
+ $psi.UseShellExecute = $false
236
+ $psi.CreateNoWindow = $true
237
+ $psi.RedirectStandardInput = $true
238
+ $psi.RedirectStandardOutput = $true
239
+ $psi.RedirectStandardError = $true
240
+ $p = [System.Diagnostics.Process]::Start($psi)
241
+ $p.StandardInput.Close()
242
+ $buf = New-Object System.IO.MemoryStream
243
+ $copy = $p.StandardOutput.BaseStream.CopyToAsync($buf)
244
+ $err = $p.StandardError.ReadToEndAsync()
245
+ $p.WaitForExit()
246
+ $copy.Wait()
247
+ $null = $err.Result
248
+ if ($p.ExitCode -ne 0) { return '' }
249
+ return ConvertFrom-TaskXmlBytes $buf.ToArray()
250
+ }
251
+
206
252
  # Before the runtime folder is swapped: Pantheon's background host (the
207
253
  # logon task) runs node.exe from it, and Windows will not rename a folder a
208
254
  # running program came from. A teammate's request the worker is running
@@ -268,6 +314,68 @@ function Install-Pantheon {
268
314
  return @{ code = $p.ExitCode; out = $out.Result; err = $err.Result }
269
315
  }
270
316
 
317
+ # Hand over to pantheon with the person's own arguments (join <invite>,
318
+ # start): after an install, and also when the update was put off because a
319
+ # teammate's run was in progress -- a join or a start needs a working
320
+ # pantheon, not the update. Sets $State.code to pantheon's exit code.
321
+ function Invoke-PantheonHandoff([string]$nodeExe, [string]$entry, [string[]]$ca, [string[]]$Arguments) {
322
+ if ($null -eq $ca) { $ca = @() }
323
+ # node.exe directly, not the .cmd: PowerShell hands arguments to a .cmd
324
+ # through cmd.exe, which would cut an invite at a & or expand a %. And
325
+ # Start-Process -NoNewWindow, so pantheon gets this console itself: a real
326
+ # terminal for the sign-in questions, not a pipe PowerShell reads from.
327
+ $State.step = 'starting pantheon'
328
+ # With the same --use-system-ca as npm and pantheon.cmd: this first run is the
329
+ # one that signs in, and behind a TLS-inspecting proxy the relay's
330
+ # certificate chains only to the company root in the Windows store.
331
+ $line = ConvertTo-ArgLine (@($ca) + @($entry) + $Arguments)
332
+ $noConsole = $false
333
+ try { $noConsole = [Console]::IsInputRedirected } catch { $noConsole = $false }
334
+ if (-not $noConsole) {
335
+ $p = Start-Process -FilePath $nodeExe -ArgumentList $line -NoNewWindow -PassThru
336
+ $null = $p.Handle # without this, ExitCode is empty once the process is gone
337
+ $p.WaitForExit()
338
+ $State.code = $p.ExitCode
339
+ return
340
+ }
341
+
342
+ # No console to type into (CI, input piped in). pantheon still runs: it
343
+ # never asks a question without a terminal (every prompt checks
344
+ # process.stdin.isTTY) and works on flags alone, so a join to a team's own
345
+ # relay, or from a machine already signed in, needs no answers. Its input
346
+ # is an empty file, so nothing can wait on it. What it cannot do here is
347
+ # sign in: on the hosted relay join and start stop at once, before anything
348
+ # is changed, with "sign in first" (or the relay's NOT_SIGNED_IN). When that
349
+ # is how it ended and config.json still has no account, the last line says
350
+ # the one thing to do; any other failure keeps pantheon's own words last.
351
+ $quiet = Join-Path ([IO.Path]::GetTempPath()) ('pantheon-' + [Guid]::NewGuid().ToString('N').Substring(0, 8))
352
+ New-Item -ItemType Directory -Force -Path $quiet | Out-Null
353
+ try {
354
+ $in = Join-Path $quiet 'in'; $errFile = Join-Path $quiet 'err'
355
+ [IO.File]::WriteAllText($in, '')
356
+ $p = Start-Process -FilePath $nodeExe -ArgumentList $line -NoNewWindow -PassThru -RedirectStandardInput $in -RedirectStandardError $errFile
357
+ $null = $p.Handle
358
+ $p.WaitForExit()
359
+ $State.code = $p.ExitCode
360
+ $said = if (Test-Path -LiteralPath $errFile) { [IO.File]::ReadAllText($errFile) } else { '' }
361
+ if ($said) { [Console]::Error.Write($said) }
362
+ if ($State.code -ne 0 -and $said -match 'sign in first|NOT_SIGNED_IN' -and $Arguments[0] -in @('join', 'start')) {
363
+ $pantheonHome = if ($env:PANTHEON_HOME) { $env:PANTHEON_HOME } else { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.pantheon' }
364
+ $signedIn = $false
365
+ try { $signedIn = [bool]((Get-Content -LiteralPath (Join-Path $pantheonHome 'config.json') -Raw | ConvertFrom-Json).account) } catch { $signedIn = $false }
366
+ if (-not $signedIn) {
367
+ # Quoted only where PowerShell needs it, so it reads as typed.
368
+ $show = ($Arguments | ForEach-Object { if ($_ -match '^[A-Za-z0-9_./:=@+,-]+$') { $_ } else { "'" + ($_ -replace "'", "''") + "'" } }) -join ' '
369
+ $what = if ($Arguments[0] -eq 'join') { 'joining' } else { 'creating your team' }
370
+ Say ''
371
+ Say "Pantheon is installed. To finish $what, open PowerShell and run: pantheon $show"
372
+ }
373
+ }
374
+ } finally {
375
+ Remove-Item -LiteralPath $quiet -Recurse -Force -ErrorAction SilentlyContinue
376
+ }
377
+ }
378
+
271
379
  try {
272
380
  if ($PantheonVersion -like '*__*') {
273
381
  Fail "this copy of install.ps1 has no version filled in. Use the one the relay serves: irm $Site/install.ps1 | iex"
@@ -457,7 +565,7 @@ function Install-Pantheon {
457
565
  $stopped = Stop-PantheonHost -NodeDir $nodeDir -HostDir $hostDir -Tasks $tasks `
458
566
  -Holders { param($d) Get-FolderHolders $d } -EndTask { param($t) $null = Invoke-Quiet $schtasks @('/End', '/TN', $t) } `
459
567
  -RunRecord (Join-Path $pantheonHomeDir 'worker-run.json') `
460
- -TaskIsMine { param($t) Test-PantheonTaskIsMine (Invoke-Quiet $schtasks @('/Query', '/TN', $t, '/XML')).out $meUser $meSid }
568
+ -TaskIsMine { param($t) Test-PantheonTaskIsMine (Get-PantheonTaskXml $schtasks $t) $meUser $meSid }
461
569
  # A teammate's run still going after the wait: nothing was stopped,
462
570
  # and the runtime is left exactly as it is (see the catch below).
463
571
  if ($stopped.busy) { throw 'PANTHEON_BUSY:' }
@@ -617,57 +725,7 @@ function Install-Pantheon {
617
725
  return
618
726
  }
619
727
  Say ''
620
- # node.exe directly, not the .cmd: PowerShell hands arguments to a .cmd
621
- # through cmd.exe, which would cut an invite at a & or expand a %. And
622
- # Start-Process -NoNewWindow, so pantheon gets this console itself: a real
623
- # terminal for the sign-in questions, not a pipe PowerShell reads from.
624
- $State.step = 'starting pantheon'
625
- $line = ConvertTo-ArgLine (@($entry) + $Arguments)
626
- $noConsole = $false
627
- try { $noConsole = [Console]::IsInputRedirected } catch { $noConsole = $false }
628
- if (-not $noConsole) {
629
- $p = Start-Process -FilePath $nodeExe -ArgumentList $line -NoNewWindow -PassThru
630
- $null = $p.Handle # without this, ExitCode is empty once the process is gone
631
- $p.WaitForExit()
632
- $State.code = $p.ExitCode
633
- return
634
- }
635
-
636
- # No console to type into (CI, input piped in). pantheon still runs: it
637
- # never asks a question without a terminal (every prompt checks
638
- # process.stdin.isTTY) and works on flags alone, so a join to a team's own
639
- # relay, or from a machine already signed in, needs no answers. Its input
640
- # is an empty file, so nothing can wait on it. What it cannot do here is
641
- # sign in: on the hosted relay join and start stop at once, before anything
642
- # is changed, with "sign in first" (or the relay's NOT_SIGNED_IN). When that
643
- # is how it ended and config.json still has no account, the last line says
644
- # the one thing to do; any other failure keeps pantheon's own words last.
645
- $quiet = Join-Path ([IO.Path]::GetTempPath()) ('pantheon-' + [Guid]::NewGuid().ToString('N').Substring(0, 8))
646
- New-Item -ItemType Directory -Force -Path $quiet | Out-Null
647
- try {
648
- $in = Join-Path $quiet 'in'; $errFile = Join-Path $quiet 'err'
649
- [IO.File]::WriteAllText($in, '')
650
- $p = Start-Process -FilePath $nodeExe -ArgumentList $line -NoNewWindow -PassThru -RedirectStandardInput $in -RedirectStandardError $errFile
651
- $null = $p.Handle
652
- $p.WaitForExit()
653
- $State.code = $p.ExitCode
654
- $said = if (Test-Path -LiteralPath $errFile) { [IO.File]::ReadAllText($errFile) } else { '' }
655
- if ($said) { [Console]::Error.Write($said) }
656
- if ($State.code -ne 0 -and $said -match 'sign in first|NOT_SIGNED_IN' -and $Arguments[0] -in @('join', 'start')) {
657
- $pantheonHome = if ($env:PANTHEON_HOME) { $env:PANTHEON_HOME } else { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.pantheon' }
658
- $signedIn = $false
659
- try { $signedIn = [bool]((Get-Content -LiteralPath (Join-Path $pantheonHome 'config.json') -Raw | ConvertFrom-Json).account) } catch { $signedIn = $false }
660
- if (-not $signedIn) {
661
- # Quoted only where PowerShell needs it, so it reads as typed.
662
- $show = ($Arguments | ForEach-Object { if ($_ -match '^[A-Za-z0-9_./:=@+,-]+$') { $_ } else { "'" + ($_ -replace "'", "''") + "'" } }) -join ' '
663
- $what = if ($Arguments[0] -eq 'join') { 'joining' } else { 'creating your team' }
664
- Say ''
665
- Say "Pantheon is installed. To finish $what, open PowerShell and run: pantheon $show"
666
- }
667
- }
668
- } finally {
669
- Remove-Item -LiteralPath $quiet -Recurse -Force -ErrorAction SilentlyContinue
670
- }
728
+ Invoke-PantheonHandoff $nodeExe $entry $ca $Arguments
671
729
  } catch {
672
730
  $m = $_.Exception.Message
673
731
  if ($m -like 'PANTHEON_BUSY:*') {
@@ -675,6 +733,22 @@ function Install-Pantheon {
675
733
  # is none that runs is the exit code non-zero.
676
734
  $have = ''
677
735
  if ($installedCli) { try { $have = & $installedCli } catch { $have = '' } }
736
+ if ($have -and $Arguments.Count -gt 0) {
737
+ # The person's join or start still happens, on the Pantheon that is
738
+ # here: it does not touch the runtime folder the run is using.
739
+ Write-Host "Pantheon was not updated to $PantheonVersion because a teammate's run was in progress; Pantheon $have is still installed and working, so your command runs on it now. Run the same line again in a few minutes to update." -ForegroundColor Yellow
740
+ if ($State.lock) { $State.lock.Dispose(); $State.lock = $null }
741
+ $caNow = @()
742
+ try { if ((Invoke-Quiet $nodeExe @('--use-system-ca', '-e', '0')).code -eq 0) { $caNow = @('--use-system-ca') } } catch { $caNow = @() }
743
+ try {
744
+ Say ''
745
+ Invoke-PantheonHandoff $nodeExe $entry $caNow $Arguments
746
+ } catch {
747
+ Write-Host "pantheon install failed: it stopped while starting pantheon ($($_.Exception.Message)). Open a new terminal and run your pantheon command again." -ForegroundColor Red
748
+ $State.code = 1
749
+ }
750
+ return
751
+ }
678
752
  $kept = if ($have) { " Pantheon $have is still installed and working." } else { '' }
679
753
  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
754
  $State.code = if ($have) { 0 } else { 1 }
@@ -508,6 +508,17 @@ add_block() {
508
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"
509
509
  return 0
510
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
511
522
  if [ "${begins:-0}" -eq 1 ]; then
512
523
  awk -v b="$BEGIN_MARK" -v e="$END_MARK" '$0 == b { keep = 1 } keep { print } $0 == e { keep = 0 }' "$f" >"$TMP_DIR/old"
513
524
  same "$TMP_DIR/old" "$TMP_DIR/block" && return 0
@@ -531,10 +542,13 @@ add_block() {
531
542
  }
532
543
 
533
544
  login_shell=$(basename "${SHELL:-sh}")
534
- # ~/.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,
535
546
  # and by the `sh -lc` Pantheon itself uses to find programs.
536
547
  add_block "$HOME/.profile"
537
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"
538
552
  { [ -f "$HOME/.bashrc" ] || [ "$login_shell" = bash ]; } && add_block "$HOME/.bashrc"
539
553
  { [ -f "$HOME/.zshrc" ] || [ "$login_shell" = zsh ]; } && add_block "$HOME/.zshrc"
540
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.33.0-rc.1",
3
+ "version": "0.33.0-rc.3",
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": {