@enterpriseai/cli 3.15.9 → 3.15.11
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 +45 -11
- package/dist/commands/classifier.d.ts.map +1 -1
- package/dist/commands/classifier.js +4 -1
- package/dist/commands/classifier.js.map +1 -1
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +88 -31
- package/dist/commands/docs.js.map +1 -1
- 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 +15 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +111 -29
- package/dist/commands/start.js.map +1 -1
- package/dist/lib/agent-guide.js +2 -2
- package/dist/lib/agent-guide.js.map +1 -1
- package/dist/lib/ai-surface-installer.d.ts +38 -0
- package/dist/lib/ai-surface-installer.d.ts.map +1 -0
- package/dist/lib/ai-surface-installer.js +160 -0
- package/dist/lib/ai-surface-installer.js.map +1 -0
- package/dist/lib/ai-surfaces.d.ts +257 -6
- package/dist/lib/ai-surfaces.d.ts.map +1 -1
- package/dist/lib/ai-surfaces.js +3182 -170
- package/dist/lib/ai-surfaces.js.map +1 -1
- package/dist/lib/api.d.ts +16 -2
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +43 -8
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/gofer-installer.js +1 -1
- package/package.json +2 -2
- package/resources/gofer/.gofer-version +17 -1
- package/resources/gofer/bash-scripts/install-optional-tools.sh +357 -16
- package/resources/gofer/bash-scripts/sync-implementation-status.sh +3 -4
- package/resources/gofer/powershell-scripts/install-optional-tools.ps1 +419 -11
- package/resources/gofer/references/platform/eai-service-patterns.md +84 -1
- package/resources/gofer/templates/visuals/capability-heatmap-template.md +2 -2
|
@@ -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
|
|
|
@@ -25,10 +25,93 @@ patterns in `eai-app-template/docs/platform/eai-service-patterns.md`.
|
|
|
25
25
|
| Resource actions | `client.resources.executeAction(type, id, action)` | named resources command if available; otherwise `eai publicapi post /v4/data/resources/...` | Actions enforce object-type rules. |
|
|
26
26
|
| Resource search | local helper around `/v4/data/resources/{tenant}/search` if SDK support is absent | `eai resources storage doctor --format json`, then `eai resources search "query" --fulltext`; use `--hybrid` or `--vector` only when doctor reports those modes ready | V4 passive ResourceAPI search is a projection over canonical data. Fulltext can be usable before semantic search modes are ready. |
|
|
27
27
|
| Resource files | local helper around resource file routes | `eai resources file upload/get/delete` | Use when the file is attached to a typed ResourceAPI object property. |
|
|
28
|
-
| Documents
|
|
28
|
+
| Documents | One `useDocuments().upload(file, context)` OR `classify([file], context)` | One `eai docs upload` OR `eai docs classify`, with authorized context | Queued ResourceAPI upload; follow the document lifecycle rules below. |
|
|
29
29
|
| Chat | `useChat(workflowId, stage).send/stream` | `eai chat send`, `eai chat stream` | Use v4 chat shape with `message`, `conversation_id`, and `params`. |
|
|
30
30
|
| Advanced PublicAPI | BFF/server helper | `eai publicapi <method> /v4/...` | Use only when named SDK/CLI support is missing. |
|
|
31
31
|
|
|
32
|
+
## Document Lifecycle Rules
|
|
33
|
+
|
|
34
|
+
The #3453 standalone lifecycle is a candidate, not deployed capability or live
|
|
35
|
+
acceptance evidence. Before generating or enabling an upload, verify the
|
|
36
|
+
installed CLI/SDK supports the context contract and the target runtime supports
|
|
37
|
+
the configured lifecycle. Provision the business document/analysis schemas and
|
|
38
|
+
Admin Portal lifecycle binding first. Storage readiness and classifier
|
|
39
|
+
readiness are separate checks; a published classifier alone is not enough.
|
|
40
|
+
|
|
41
|
+
- Use one `POST /v4/data/documents/upload` for durable upload and queued
|
|
42
|
+
classification, with `storage_target=resourceapi`. Browser callers use the
|
|
43
|
+
local BFF `/api/eai/v4/data/documents/upload`; tokens stay server-side.
|
|
44
|
+
- For standalone documents, send both `verticalKey` and `workflowKey`. PublicAPI
|
|
45
|
+
validates the authorised app/workflow and resolves optional
|
|
46
|
+
`config.documentLifecycle` on the existing classifier-target
|
|
47
|
+
`vertical-product-config` binding, never from the upload body.
|
|
48
|
+
- Allowed binding values are `planning-assist-v1`, `planning-assess-v1`, and
|
|
49
|
+
`business-document-v1`. Omission preserves existing behaviour and does not
|
|
50
|
+
erase a saved selection on reassociation. Unsupported values fail validation.
|
|
51
|
+
Missing planning fields do not imply business mode.
|
|
52
|
+
- Preserve working DAISY/Assess requests, planning/case relationships, rules,
|
|
53
|
+
stored records and in-flight jobs. Retain real authorised
|
|
54
|
+
`planning_application_id`, `business_request_id` and applicable
|
|
55
|
+
`assess_case_id` context (SDK: `planningApplicationId`, `businessRequestId`,
|
|
56
|
+
`assessCaseId`). Never fabricate a planning application, business request,
|
|
57
|
+
case or form submission for a standalone document. Optional real parents
|
|
58
|
+
must be supported by the selected lifecycle and authorised.
|
|
59
|
+
- Call `classify([file], context)` for `processing_mode=classification`, OR
|
|
60
|
+
`upload(file, context)` for full requested processing. Do not upload and then
|
|
61
|
+
classify the same bytes again. The SDK supplies `storage_target=resourceapi`.
|
|
62
|
+
Do not generate context-free legacy file classification or a fallback to it.
|
|
63
|
+
- Do not send lifecycle mappings, target collections, permissions, provider
|
|
64
|
+
credentials or worker pins from the client. Missing schema, storage,
|
|
65
|
+
service, app/workflow or classifier readiness must fail before upload-side
|
|
66
|
+
writes, not fall back to legacy storage or an unscoped classifier.
|
|
67
|
+
- Retain the returned job/document IDs. Poll
|
|
68
|
+
`GET /v4/data/documents/jobs/{job_id}` with authorised context and read back
|
|
69
|
+
saved requested stages and provenance. Acknowledgement and provider success
|
|
70
|
+
are not completed classification, extraction, rule validation or persistence.
|
|
71
|
+
A polling timeout is incomplete; never re-upload automatically.
|
|
72
|
+
- Indexing is an optional derived stage only where supported and requested.
|
|
73
|
+
Use authorised lifecycle file retrieval and cleanup. Invalidate stale work
|
|
74
|
+
and clean only owned outputs according to retention, never shared parents.
|
|
75
|
+
- Direct `POST /v4/data/documents/classify-by-url` is analysis, not a durable
|
|
76
|
+
queued upload/save/readback/cleanup replacement. New workflow-selected URL
|
|
77
|
+
callers also supply the app/workflow pair. Preserve established unscoped
|
|
78
|
+
DAISY/Assess classifier behaviour without making it a new business-app default.
|
|
79
|
+
|
|
80
|
+
Candidate example after provisioning and version checks, submitted once:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const { classify } = useDocuments(tenantId);
|
|
84
|
+
const response = await classify([file], {
|
|
85
|
+
verticalKey: appKey,
|
|
86
|
+
workflowKey,
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) throw new Error("Document submission failed.");
|
|
89
|
+
const accepted = await response.json();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Retain the returned IDs and implement bounded status polling plus saved-result
|
|
93
|
+
readback before reporting success. The equivalent CLI submission is:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
eai docs classify ./document.pdf --tenant-id <tenant-id> \
|
|
97
|
+
--storage-target resourceapi --vertical-key <app-key> --workflow-key <workflow-key>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
An authorised administrator selects the binding with
|
|
101
|
+
`eai classifier target <classifier-key> --app <app-key> --workflow <workflow-key>
|
|
102
|
+
--document-lifecycle business-document-v1` after schema/storage and published
|
|
103
|
+
classifier readiness checks. Verify this syntax with the installed command's
|
|
104
|
+
`--help`. `--document-lifecycle` is a target-administration option, not an upload
|
|
105
|
+
option.
|
|
106
|
+
|
|
107
|
+
Scope is document-use migration, not all v3 retirement. Existing-record
|
|
108
|
+
read/download/delete/status, old queued callbacks, reference files and real
|
|
109
|
+
form attachments remain compatibility obligations. Replacement, client and
|
|
110
|
+
tenant-setup proof must precede enforcement against approved new legacy
|
|
111
|
+
admissions. Track package/plugin/docs publication and installed adoption
|
|
112
|
+
separately from local source or generator checks; do not claim live acceptance.
|
|
113
|
+
|
|
114
|
+
|
|
32
115
|
## Storage Backend Rules
|
|
33
116
|
|
|
34
117
|
Keep Object Type identifiers in their correct layer. Configuration/model
|
|
@@ -30,9 +30,9 @@ quadrantChart
|
|
|
30
30
|
x-axis Low Maturity --> High Maturity
|
|
31
31
|
y-axis Low Value --> High Value
|
|
32
32
|
quadrant-1 Strategic Investment
|
|
33
|
-
quadrant-2
|
|
33
|
+
quadrant-2 Quick Wins
|
|
34
34
|
quadrant-3 Deprecate
|
|
35
|
-
quadrant-4
|
|
35
|
+
quadrant-4 Optimize
|
|
36
36
|
{{CAPABILITY_1}}: [{{X1}}, {{Y1}}]
|
|
37
37
|
{{CAPABILITY_2}}: [{{X2}}, {{Y2}}]
|
|
38
38
|
{{CAPABILITY_3}}: [{{X3}}, {{Y3}}]
|