@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.
- package/README.md +37 -9
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +59 -12
- package/dist/commands/start.js.map +1 -1
- package/dist/lib/ai-surfaces.d.ts +242 -6
- package/dist/lib/ai-surfaces.d.ts.map +1 -1
- package/dist/lib/ai-surfaces.js +3103 -174
- package/dist/lib/ai-surfaces.js.map +1 -1
- package/dist/lib/gofer-installer.js +1 -1
- package/package.json +1 -1
- package/resources/gofer/.gofer-version +7 -1
- package/resources/gofer/bash-scripts/install-optional-tools.sh +357 -16
- package/resources/gofer/powershell-scripts/install-optional-tools.ps1 +419 -11
|
@@ -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 =
|
|
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
|
|
165
|
-
Write-Info "Selected 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-
|
|
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-
|
|
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-
|
|
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
|
-
'
|
|
186
|
-
Install-
|
|
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 '
|
|
612
|
+
Write-Info ' agy'
|
|
613
|
+
Write-Info ' grok'
|
|
206
614
|
Write-Info ' gh auth login'
|
|
207
615
|
Write-Info ' az login'
|
|
208
616
|
|