@enterpriseai/cli 3.15.9 → 3.15.10

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.
@@ -5,6 +5,7 @@ param(
5
5
 
6
6
  $ErrorActionPreference = 'Stop'
7
7
  $HasFailure = $false
8
+ $LastStepSucceeded = $true
8
9
 
9
10
  function Write-Info {
10
11
  param([string]$Message)
@@ -16,6 +17,41 @@ function Write-Warn {
16
17
  Write-Warning "[gofer] $Message"
17
18
  }
18
19
 
20
+ function ConvertTo-SafeDiagnostic {
21
+ param([string]$Message)
22
+
23
+ if ([string]::IsNullOrEmpty($Message)) {
24
+ return 'No diagnostic details were provided.'
25
+ }
26
+
27
+ $safeMessage = $Message
28
+ foreach ($localRoot in @(
29
+ $WorkspacePath,
30
+ $HOME,
31
+ $env:USERPROFILE,
32
+ [System.IO.Path]::GetTempPath()
33
+ )) {
34
+ if (-not [string]::IsNullOrWhiteSpace($localRoot)) {
35
+ $safeMessage = [regex]::Replace(
36
+ $safeMessage,
37
+ [regex]::Escape($localRoot),
38
+ '<local-path>',
39
+ [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
40
+ )
41
+ }
42
+ }
43
+ $safeMessage = $safeMessage `
44
+ -replace '(?i)\b((?:api[-_ ]?key|token|secret|password|credential)\s*[:=]\s*)[^\s,;]+', '$1<redacted>' `
45
+ -replace '(?i)(https?://)[^\s/@:]+:[^\s/@]+@', '$1<redacted>@' `
46
+ -replace '(?i)\b[A-Z]:\\Users\\[^\\\s"'']+', '<home>' `
47
+ -replace '/(?:Users|home)/[^/\s"'']+', '<home>' `
48
+ -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', ''
49
+ if ($safeMessage.Length -gt 4096) {
50
+ return $safeMessage.Substring(0, 4096) + '...<truncated>'
51
+ }
52
+ return $safeMessage
53
+ }
54
+
19
55
  function Invoke-Step {
20
56
  param(
21
57
  [string]$Description,
@@ -23,12 +59,14 @@ function Invoke-Step {
23
59
  )
24
60
 
25
61
  Write-Info $Description
62
+ $script:LastStepSucceeded = $true
26
63
  try {
27
64
  & $ScriptBlock
28
65
  } catch {
66
+ $script:LastStepSucceeded = $false
29
67
  $script:HasFailure = $true
30
68
  Write-Warn "Failed: $Description"
31
- Write-Warn $_.Exception.Message
69
+ Write-Warn (ConvertTo-SafeDiagnostic $_.Exception.Message)
32
70
  }
33
71
  }
34
72
 
@@ -37,6 +75,36 @@ function Test-CommandExists {
37
75
  return $null -ne (Get-Command $CommandName -ErrorAction SilentlyContinue)
38
76
  }
39
77
 
78
+ function Stop-InstallerProcess {
79
+ param([System.Diagnostics.Process]$Process)
80
+
81
+ if ($null -eq $Process) { return }
82
+ try {
83
+ if (-not $Process.HasExited) {
84
+ try {
85
+ # Kill the complete process tree where the current runtime supports it.
86
+ $Process.Kill($true)
87
+ } catch {
88
+ # Windows PowerShell 5.1 exposes only Kill() without the tree overload.
89
+ # taskkill is the operating-system fallback for terminating descendants.
90
+ $taskkillPath = if ($env:SystemRoot) {
91
+ Join-Path $env:SystemRoot 'System32\taskkill.exe'
92
+ } else {
93
+ $null
94
+ }
95
+ if ($taskkillPath -and [System.IO.File]::Exists($taskkillPath)) {
96
+ & $taskkillPath /PID $Process.Id /T /F *> $null
97
+ } else {
98
+ $Process.Kill()
99
+ }
100
+ }
101
+ [void]$Process.WaitForExit(5000)
102
+ }
103
+ } catch {
104
+ # Best-effort cleanup must not hide the original interruption or failure.
105
+ }
106
+ }
107
+
40
108
  function Get-JsPackageManager {
41
109
  if (Test-Path (Join-Path $WorkspacePath 'bun.lockb')) {
42
110
  return 'bun'
@@ -109,6 +177,301 @@ function Install-NpmGlobalPackage {
109
177
  Invoke-Step "Installing $PackageName globally with npm" { npm install --global $PackageName }
110
178
  }
111
179
 
180
+ function Test-InstalledCli {
181
+ param(
182
+ [string]$ToolName,
183
+ [string[]]$ExpectedPaths
184
+ )
185
+
186
+ Invoke-Step "Validating the installed $ToolName executable path" {
187
+ foreach ($candidatePath in $ExpectedPaths) {
188
+ if ([string]::IsNullOrWhiteSpace($candidatePath) -or
189
+ -not [System.IO.Path]::IsPathRooted($candidatePath)) {
190
+ continue
191
+ }
192
+ $fullPath = [System.IO.Path]::GetFullPath($candidatePath)
193
+ if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
194
+ $item = Get-Item -LiteralPath $fullPath -Force
195
+ if ($item.Length -gt 0) {
196
+ Write-Info "$ToolName installer produced its provider-defined executable"
197
+ return
198
+ }
199
+ }
200
+ }
201
+ throw "$ToolName installation did not produce its expected executable."
202
+ }
203
+ }
204
+
205
+ function Install-PinnedCopilotCli {
206
+ $packageSpec = '@github/copilot@1.0.83'
207
+ $expectedIntegrity = 'sha512-M8uZI0V0dahYV1KZij3nGDxaXEGG7I7YUZzQPI7NEZkL/83Nl/tNTbPdxKtdWZbOmWoXsPKXty/eEYoj6RHDhA=='
208
+ $registry = 'https://registry.npmjs.org/'
209
+
210
+ $npmCommand = Get-Command -Name 'npm' -CommandType Application -All -ErrorAction SilentlyContinue |
211
+ Select-Object -First 1
212
+ if ($null -eq $npmCommand -or -not [System.IO.File]::Exists($npmCommand.Source)) {
213
+ $script:HasFailure = $true
214
+ Write-Warn "npm is required to install $packageSpec globally"
215
+ return
216
+ }
217
+
218
+ $script:CopilotCommandPaths = @()
219
+ Invoke-Step 'Verifying and installing the pinned GitHub Copilot CLI' {
220
+ $integrityOutput = (& $npmCommand.Source view $packageSpec dist.integrity --json --registry=$registry 2>$null | Out-String).Trim()
221
+ if ($LASTEXITCODE -ne 0) {
222
+ throw 'Could not resolve the pinned GitHub Copilot CLI package.'
223
+ }
224
+ $actualIntegrity = $integrityOutput.Trim('"')
225
+ if ($actualIntegrity -cne $expectedIntegrity) {
226
+ throw 'Refusing GitHub Copilot CLI because its npm integrity changed.'
227
+ }
228
+
229
+ & $npmCommand.Source install --global $packageSpec --registry=$registry `
230
+ --ignore-scripts --no-audit --no-fund *> $null
231
+ if ($LASTEXITCODE -ne 0) {
232
+ throw 'The pinned GitHub Copilot CLI installation failed.'
233
+ }
234
+
235
+ & $npmCommand.Source list --global --depth=0 $packageSpec *> $null
236
+ if ($LASTEXITCODE -ne 0) {
237
+ throw 'The installed GitHub Copilot CLI package did not match the pinned version.'
238
+ }
239
+
240
+ $npmPrefix = (& $npmCommand.Source prefix --global 2>$null | Out-String).Trim()
241
+ if ($LASTEXITCODE -ne 0 -or -not [System.IO.Path]::IsPathRooted($npmPrefix)) {
242
+ throw 'Could not resolve an absolute npm prefix for GitHub Copilot CLI.'
243
+ }
244
+ $script:CopilotCommandPaths = @(
245
+ (Join-Path $npmPrefix 'copilot.cmd'),
246
+ (Join-Path $npmPrefix 'copilot.exe'),
247
+ (Join-Path $npmPrefix 'bin\copilot')
248
+ )
249
+ }
250
+ if ($script:LastStepSucceeded) {
251
+ Test-InstalledCli -ToolName 'GitHub Copilot CLI' -ExpectedPaths $script:CopilotCommandPaths
252
+ }
253
+ }
254
+
255
+ function Get-OfficialInstallerPayload {
256
+ param(
257
+ [string]$ToolName,
258
+ [string]$ScriptUrl,
259
+ [string]$AllowedFinalOrigin
260
+ )
261
+
262
+ $initialUri = [System.Uri]$ScriptUrl
263
+ $finalOriginUri = [System.Uri]$AllowedFinalOrigin
264
+ if (-not $initialUri.IsAbsoluteUri -or $initialUri.Scheme -cne 'https' -or $initialUri.UserInfo) {
265
+ throw "$ToolName installer URL is not an absolute HTTPS URL."
266
+ }
267
+ if (-not $finalOriginUri.IsAbsoluteUri -or $finalOriginUri.Scheme -cne 'https' -or
268
+ $finalOriginUri.UserInfo -or $finalOriginUri.AbsolutePath -cne '/' -or
269
+ $finalOriginUri.Query -or $finalOriginUri.Fragment) {
270
+ throw "$ToolName final installer origin is invalid."
271
+ }
272
+
273
+ Add-Type -AssemblyName System.Net.Http -ErrorAction Stop
274
+ $handler = [System.Net.Http.HttpClientHandler]::new()
275
+ $handler.AllowAutoRedirect = $false
276
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
277
+ $httpClient.Timeout = [TimeSpan]::FromMinutes(2)
278
+ $httpClient.MaxResponseContentBufferSize = 1MB
279
+ $allowedOrigins = @(
280
+ $initialUri.GetLeftPart([System.UriPartial]::Authority),
281
+ $finalOriginUri.GetLeftPart([System.UriPartial]::Authority)
282
+ ) | Select-Object -Unique
283
+
284
+ $currentUri = $initialUri
285
+ $response = $null
286
+ try {
287
+ for ($redirectCount = 0; $redirectCount -le 5; $redirectCount += 1) {
288
+ $currentOrigin = $currentUri.GetLeftPart([System.UriPartial]::Authority)
289
+ if ($currentUri.Scheme -cne 'https' -or $currentUri.UserInfo -or
290
+ $allowedOrigins -cnotcontains $currentOrigin) {
291
+ throw "$ToolName installer redirected to an unapproved origin."
292
+ }
293
+
294
+ $request = [System.Net.Http.HttpRequestMessage]::new(
295
+ [System.Net.Http.HttpMethod]::Get,
296
+ $currentUri
297
+ )
298
+ try {
299
+ $response = $httpClient.SendAsync(
300
+ $request,
301
+ [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead
302
+ ).GetAwaiter().GetResult()
303
+ } finally {
304
+ $request.Dispose()
305
+ }
306
+
307
+ $statusCode = [int]$response.StatusCode
308
+ if ($statusCode -in @(301, 302, 303, 307, 308)) {
309
+ $redirectLocation = $response.Headers.Location
310
+ if ($null -eq $redirectLocation) {
311
+ throw "$ToolName installer redirect omitted its destination."
312
+ }
313
+ $nextUri = if ($redirectLocation.IsAbsoluteUri) {
314
+ $redirectLocation
315
+ } else {
316
+ [System.Uri]::new($currentUri, $redirectLocation)
317
+ }
318
+ $response.Dispose()
319
+ $response = $null
320
+ $currentUri = $nextUri
321
+ continue
322
+ }
323
+
324
+ [void]$response.EnsureSuccessStatusCode()
325
+ if ($currentOrigin -cne $finalOriginUri.GetLeftPart([System.UriPartial]::Authority)) {
326
+ throw "$ToolName installer did not finish at its approved provider origin."
327
+ }
328
+ if ($response.Content.Headers.ContentLength.HasValue -and
329
+ $response.Content.Headers.ContentLength.Value -gt 1MB) {
330
+ throw "$ToolName installer exceeded the allowed download size."
331
+ }
332
+ $contentStream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
333
+ $buffer = New-Object byte[] 8192
334
+ $memory = [System.IO.MemoryStream]::new()
335
+ try {
336
+ while (($bytesRead = $contentStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
337
+ if (($memory.Length + $bytesRead) -gt 1MB) {
338
+ throw "$ToolName installer exceeded the allowed download size."
339
+ }
340
+ $memory.Write($buffer, 0, $bytesRead)
341
+ }
342
+ $installerBytes = $memory.ToArray()
343
+ } finally {
344
+ $memory.Dispose()
345
+ $contentStream.Dispose()
346
+ }
347
+ if ($installerBytes.Length -eq 0) {
348
+ throw "$ToolName installer was empty."
349
+ }
350
+ return [pscustomobject]@{
351
+ Bytes = $installerBytes
352
+ EffectiveUrl = $currentUri.AbsoluteUri
353
+ }
354
+ }
355
+ throw "$ToolName installer exceeded the redirect limit."
356
+ } finally {
357
+ if ($null -ne $response) { $response.Dispose() }
358
+ $httpClient.Dispose()
359
+ $handler.Dispose()
360
+ }
361
+ }
362
+
363
+ function Install-FromOfficialPowerShellScript {
364
+ param(
365
+ [string]$ToolName,
366
+ [string]$ScriptUrl,
367
+ [string]$ExpectedSha256,
368
+ [string]$AllowedFinalOrigin
369
+ )
370
+
371
+ Invoke-Step "Installing or updating $ToolName from $ScriptUrl" {
372
+ $installerBytes = $null
373
+ $installerText = $null
374
+ try {
375
+ $payload = Get-OfficialInstallerPayload -ToolName $ToolName `
376
+ -ScriptUrl $ScriptUrl -AllowedFinalOrigin $AllowedFinalOrigin
377
+ $installerBytes = [byte[]]$payload.Bytes
378
+ $sha256 = [System.Security.Cryptography.SHA256]::Create()
379
+ try {
380
+ $actualSha256 = [System.BitConverter]::ToString(
381
+ $sha256.ComputeHash($installerBytes)
382
+ ).Replace('-', '').ToLowerInvariant()
383
+ } finally {
384
+ $sha256.Dispose()
385
+ }
386
+ if ($actualSha256 -cne $ExpectedSha256) {
387
+ throw "$ToolName installer SHA-256 digest changed."
388
+ }
389
+
390
+ $strictUtf8 = [System.Text.UTF8Encoding]::new($false, $true)
391
+ $installerText = $strictUtf8.GetString($installerBytes)
392
+ if ($installerText.Length -gt 0 -and $installerText[0] -eq [char]0xFEFF) {
393
+ $installerText = $installerText.Substring(1)
394
+ }
395
+
396
+ $powerShell = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName
397
+ if (-not [System.IO.File]::Exists($powerShell)) {
398
+ throw 'Could not resolve the current PowerShell executable.'
399
+ }
400
+
401
+ $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
402
+ $startInfo.FileName = $powerShell
403
+ # Feed the verified bytes over a private pipe. No pathname exists for a
404
+ # second process to replace between digest verification and execution.
405
+ $startInfo.Arguments = '-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'
406
+ $startInfo.UseShellExecute = $false
407
+ $startInfo.CreateNoWindow = $true
408
+ $startInfo.RedirectStandardInput = $true
409
+ # Provider output is intentionally drained without relaying it. Even a
410
+ # verified installer can print local paths or account data.
411
+ $startInfo.RedirectStandardOutput = $true
412
+ $startInfo.RedirectStandardError = $true
413
+ $startInfo.EnvironmentVariables.Clear()
414
+ foreach ($environmentName in @(
415
+ 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP', 'HOME',
416
+ 'USERPROFILE', 'LOCALAPPDATA', 'APPDATA', 'PROGRAMDATA', 'PROGRAMFILES',
417
+ 'PROGRAMFILES(X86)', 'ProgramW6432', 'HOMEDRIVE', 'HOMEPATH', 'USERNAME', 'LANG',
418
+ 'OS', 'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER', 'NUMBER_OF_PROCESSORS',
419
+ 'DOTNET_ROOT', 'DOTNET_ROOT_ARM64', 'DOTNET_ROOT_X64'
420
+ )) {
421
+ $environmentValue = [System.Environment]::GetEnvironmentVariable($environmentName)
422
+ if (-not [string]::IsNullOrEmpty($environmentValue)) {
423
+ $startInfo.EnvironmentVariables[$environmentName] = $environmentValue
424
+ }
425
+ }
426
+ $systemPath = @(
427
+ (Join-Path $env:SystemRoot 'System32'),
428
+ $env:SystemRoot,
429
+ (Split-Path -Parent $powerShell)
430
+ ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
431
+ $startInfo.EnvironmentVariables['PATH'] = $systemPath -join ';'
432
+ $startInfo.EnvironmentVariables['CODEX_NON_INTERACTIVE'] = '1'
433
+
434
+ $installerProcess = [System.Diagnostics.Process]::new()
435
+ $installerProcess.StartInfo = $startInfo
436
+ try {
437
+ if (-not $installerProcess.Start()) {
438
+ throw "Could not start the isolated $ToolName installer process."
439
+ }
440
+ $stdoutDrain = $installerProcess.StandardOutput.BaseStream.CopyToAsync([System.IO.Stream]::Null)
441
+ $stderrDrain = $installerProcess.StandardError.BaseStream.CopyToAsync([System.IO.Stream]::Null)
442
+ $stdinWrite = $installerProcess.StandardInput.WriteAsync($installerText)
443
+ if (-not $stdinWrite.Wait([TimeSpan]::FromSeconds(15))) {
444
+ throw "$ToolName installer input pipe timed out."
445
+ }
446
+ $installerProcess.StandardInput.Close()
447
+
448
+ $deadline = [DateTime]::UtcNow.AddMinutes(15)
449
+ while (-not $installerProcess.WaitForExit(250)) {
450
+ if ([DateTime]::UtcNow -ge $deadline) {
451
+ throw "$ToolName installer exceeded its bounded execution time."
452
+ }
453
+ }
454
+ if (-not [System.Threading.Tasks.Task]::WaitAll(
455
+ [System.Threading.Tasks.Task[]]@($stdoutDrain, $stderrDrain),
456
+ [TimeSpan]::FromSeconds(5)
457
+ )) {
458
+ throw "$ToolName installer output pipes did not close."
459
+ }
460
+ if ($installerProcess.ExitCode -ne 0) {
461
+ throw "$ToolName installer exited with code $($installerProcess.ExitCode)"
462
+ }
463
+ } finally {
464
+ Stop-InstallerProcess -Process $installerProcess
465
+ $installerProcess.Dispose()
466
+ }
467
+ } finally {
468
+ $installerText = $null
469
+ $installerBytes = $null
470
+ }
471
+ }
472
+ return $script:LastStepSucceeded
473
+ }
474
+
112
475
  function Install-GitHubCli {
113
476
  if (Test-CommandExists 'gh') {
114
477
  Write-Info 'GitHub CLI is already installed'
@@ -158,11 +521,23 @@ if ([string]::IsNullOrWhiteSpace($Tools)) {
158
521
  exit 0
159
522
  }
160
523
 
161
- $toolList = $Tools.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)
524
+ $toolList = @()
525
+ foreach ($rawTool in $Tools.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)) {
526
+ $normalizedTool = $rawTool.Trim().ToLowerInvariant()
527
+ if ($normalizedTool -in @('agy', 'gemini')) {
528
+ $normalizedTool = 'antigravity'
529
+ }
530
+ if ($normalizedTool -notmatch '^[a-z0-9-]+$') {
531
+ $normalizedTool = 'invalid-tool-id'
532
+ }
533
+ if ($normalizedTool -and $normalizedTool -notin $toolList) {
534
+ $toolList += $normalizedTool
535
+ }
536
+ }
162
537
  $packageManager = Get-JsPackageManager
163
538
 
164
- Write-Info "Workspace: $WorkspacePath"
165
- Write-Info "Selected tools: $Tools"
539
+ Write-Info 'Workspace selected'
540
+ Write-Info "Selected tools: $($toolList -join ',')"
166
541
 
167
542
  foreach ($tool in $toolList) {
168
543
  switch ($tool.Trim()) {
@@ -174,16 +549,48 @@ foreach ($tool in $toolList) {
174
549
  Install-PlaywrightBrowsers
175
550
  }
176
551
  'claude' {
177
- Install-NpmGlobalPackage -PackageName '@anthropic-ai/claude-code'
552
+ if (Install-FromOfficialPowerShellScript -ToolName 'Claude Code' `
553
+ -ScriptUrl 'https://claude.ai/install.ps1' `
554
+ -ExpectedSha256 'cd17c6b555f761d60373659824bf805e1510538226e4c7028e19d7494937a333' `
555
+ -AllowedFinalOrigin 'https://downloads.claude.ai') {
556
+ Test-InstalledCli -ToolName 'Claude Code' -ExpectedPaths @(
557
+ (Join-Path $env:USERPROFILE '.local\bin\claude.exe'),
558
+ (Join-Path $env:USERPROFILE '.local\bin\claude')
559
+ )
560
+ }
178
561
  }
179
562
  'codex' {
180
- Install-NpmGlobalPackage -PackageName '@openai/codex-cli'
563
+ if (Install-FromOfficialPowerShellScript -ToolName 'Codex CLI' `
564
+ -ScriptUrl 'https://chatgpt.com/codex/install.ps1' `
565
+ -ExpectedSha256 '391f247de2c70c7e99041979ec02dae7e76be27ac9cfc1dfe7c1eb21d48d8b97' `
566
+ -AllowedFinalOrigin 'https://releases.openai.com') {
567
+ Test-InstalledCli -ToolName 'Codex CLI' -ExpectedPaths @(
568
+ (Join-Path $env:LOCALAPPDATA 'Programs\OpenAI\Codex\bin\codex.exe')
569
+ )
570
+ }
181
571
  }
182
572
  'copilot' {
183
- Install-NpmGlobalPackage -PackageName '@github/copilot'
573
+ Install-PinnedCopilotCli
574
+ }
575
+ 'antigravity' {
576
+ if (Install-FromOfficialPowerShellScript -ToolName 'Antigravity CLI (agy)' `
577
+ -ScriptUrl 'https://antigravity.google/cli/install.ps1' `
578
+ -ExpectedSha256 '51c2cb4fada22ce0228da71b9506370383d6544bfebcec85fe7616a52b805344' `
579
+ -AllowedFinalOrigin 'https://antigravity.google') {
580
+ Test-InstalledCli -ToolName 'Antigravity CLI' -ExpectedPaths @(
581
+ (Join-Path $env:LOCALAPPDATA 'agy\bin\agy.exe')
582
+ )
583
+ }
184
584
  }
185
- 'gemini' {
186
- Install-NpmGlobalPackage -PackageName '@google/gemini-cli'
585
+ 'grok' {
586
+ if (Install-FromOfficialPowerShellScript -ToolName 'Grok Build' `
587
+ -ScriptUrl 'https://x.ai/cli/install.ps1' `
588
+ -ExpectedSha256 '3a4ee2b1d744252c00827abbdeb2589f6b3dae80e73d88e0a81e08dc0ee747e7' `
589
+ -AllowedFinalOrigin 'https://x.ai') {
590
+ Test-InstalledCli -ToolName 'Grok Build' -ExpectedPaths @(
591
+ (Join-Path $env:USERPROFILE '.grok\bin\grok.exe')
592
+ )
593
+ }
187
594
  }
188
595
  'gh' {
189
596
  Install-GitHubCli
@@ -199,10 +606,11 @@ foreach ($tool in $toolList) {
199
606
  }
200
607
 
201
608
  Write-Info 'Suggested next steps:'
202
- Write-Info ' claude login'
609
+ Write-Info ' claude auth login'
203
610
  Write-Info ' codex login'
204
611
  Write-Info ' copilot login'
205
- Write-Info ' gemini auth login'
612
+ Write-Info ' agy'
613
+ Write-Info ' grok'
206
614
  Write-Info ' gh auth login'
207
615
  Write-Info ' az login'
208
616