@darkrei08/setup-ai 3.0.5 → 3.3.0
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 +35 -28
- package/bin/setup-ai.mjs +48 -23
- package/package.json +4 -3
- package/setup-ai.ps1 +618 -127
- package/setup-ai.sh +592 -207
package/setup-ai.ps1
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#Requires -Version 7.
|
|
1
|
+
#Requires -Version 7.3
|
|
2
2
|
<#
|
|
3
3
|
==============================================================================
|
|
4
4
|
AI Dev Suite — Engineering Excellence Edition (Windows)
|
|
5
|
-
Version: 3.0
|
|
5
|
+
Version: 3.3.0
|
|
6
6
|
|
|
7
7
|
Windows-native installer, sibling of setup-ai.sh. Uses each tool's official
|
|
8
8
|
Windows method: winget for language runtimes, the vendor install.ps1 scripts
|
|
9
|
-
for the AI CLIs,
|
|
9
|
+
for the AI CLIs, and npm for opencode, and
|
|
10
10
|
`npx skills` / `pi install` for skills and pi packages.
|
|
11
11
|
|
|
12
12
|
The Node launcher bin/setup-ai.mjs dispatches here on win32 and can pass a
|
|
@@ -30,19 +30,29 @@ param(
|
|
|
30
30
|
[switch]$Help
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
$OnlySpecified = $PSBoundParameters.ContainsKey('Only')
|
|
34
|
+
|
|
33
35
|
$ErrorActionPreference = 'Stop'
|
|
34
36
|
Set-StrictMode -Version Latest
|
|
35
37
|
|
|
38
|
+
# PowerShell 7.3+ turns native nonzero exits into terminating errors, so a
|
|
39
|
+
# failed command cannot be hidden by a later successful command in the same step.
|
|
40
|
+
$PSNativeCommandUseErrorActionPreference = $true
|
|
41
|
+
|
|
36
42
|
# Render child-process UTF-8 output (npx skills box-drawing, banners) correctly
|
|
37
43
|
# instead of mojibake on the default Windows console codepage.
|
|
38
44
|
try {
|
|
39
45
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
40
46
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
41
47
|
if (Get-Command chcp -ErrorAction SilentlyContinue) { chcp 65001 | Out-Null }
|
|
42
|
-
} catch {
|
|
48
|
+
} catch {
|
|
49
|
+
# Non-fatal: child output may show mojibake, but the run can proceed.
|
|
50
|
+
Write-Warning "Could not set UTF-8 console encoding: $($_.Exception.Message)"
|
|
51
|
+
}
|
|
43
52
|
|
|
44
|
-
$ScriptVersion = "3.0
|
|
45
|
-
$
|
|
53
|
+
$ScriptVersion = "3.3.0"
|
|
54
|
+
$ScriptPath = $PSCommandPath
|
|
55
|
+
$ScriptDir = Split-Path -Parent $ScriptPath
|
|
46
56
|
$LogDir = Join-Path $ScriptDir "logs"
|
|
47
57
|
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
|
48
58
|
|
|
@@ -51,11 +61,53 @@ $HumanLog = Join-Path $LogDir "setup_$RunId.log"
|
|
|
51
61
|
$JsonlLog = Join-Path $LogDir "setup_$RunId.jsonl"
|
|
52
62
|
$ReportFile = Join-Path $LogDir "engineering-report_$RunId.md"
|
|
53
63
|
|
|
54
|
-
$EE_Slug = "
|
|
64
|
+
$EE_Slug = "darkrei08/Engineering-Excellence"
|
|
55
65
|
$EE_Skill = "engineering-excellence"
|
|
56
|
-
$PiSkillDir = Join-Path $HOME ".pi\agent\skills\$EE_Skill"
|
|
57
66
|
$PiExtDir = Join-Path $HOME ".pi\agent\extensions"
|
|
58
67
|
|
|
68
|
+
# Upstream agent-skill stack mirrored from vekexasia/dotenv setup_env.sh so the
|
|
69
|
+
# same skills land on every OS (dotenv itself is Linux-only). Installed via
|
|
70
|
+
# `npx skills add`.
|
|
71
|
+
$UpstreamSkillSources = @(
|
|
72
|
+
@{ Source = 'herdrdev/herdr'; Skills = @('herdr') }
|
|
73
|
+
@{ Source = 'mattpocock/skills'; Skills = @('triage','grill-me','grilling','wayfinder','domain-modeling','prototype','research') }
|
|
74
|
+
@{ Source = 'https://github.com/pedronauck/skills'; Skills = @('typescript-advanced') }
|
|
75
|
+
@{ Source = 'humanlayer/skills'; Skills = @('show-me') }
|
|
76
|
+
)
|
|
77
|
+
$UpstreamSkillNames = @('herdr','triage','grill-me','grilling','wayfinder','domain-modeling','prototype','research','typescript-advanced','show-me')
|
|
78
|
+
$SkillAgentNames = @('pi','claude-code','gemini-cli','cursor','antigravity','codex','opencode')
|
|
79
|
+
$SkillAgentConfigDirs = @{
|
|
80
|
+
pi = Join-Path $HOME ".pi"
|
|
81
|
+
'claude-code' = Join-Path $HOME ".claude"
|
|
82
|
+
'gemini-cli' = Join-Path $HOME ".gemini"
|
|
83
|
+
cursor = Join-Path $HOME ".cursor"
|
|
84
|
+
antigravity = Join-Path $HOME ".antigravity"
|
|
85
|
+
codex = Join-Path $HOME ".codex"
|
|
86
|
+
opencode = Join-Path $HOME ".config\opencode"
|
|
87
|
+
}
|
|
88
|
+
$SkillAgentRoots = @{
|
|
89
|
+
pi = Join-Path $HOME ".pi\agent\skills"
|
|
90
|
+
'claude-code' = Join-Path $HOME ".claude\skills"
|
|
91
|
+
'gemini-cli' = Join-Path $HOME ".gemini\skills"
|
|
92
|
+
cursor = Join-Path $HOME ".cursor\skills"
|
|
93
|
+
antigravity = Join-Path $HOME ".antigravity\skills"
|
|
94
|
+
codex = Join-Path $HOME ".codex\skills"
|
|
95
|
+
opencode = Join-Path $HOME ".config\opencode\skills"
|
|
96
|
+
}
|
|
97
|
+
# Candidate skill roots per agent: verification passes if SKILL.md exists in any
|
|
98
|
+
# of them. Every agent keeps its single existing root; Codex also accepts
|
|
99
|
+
# ~/.agents/skills because upstream `skills add --global` writes Codex skills
|
|
100
|
+
# there instead of ~/.codex/skills.
|
|
101
|
+
$SkillAgentCandidateRoots = @{
|
|
102
|
+
pi = @($SkillAgentRoots['pi'])
|
|
103
|
+
'claude-code' = @($SkillAgentRoots['claude-code'])
|
|
104
|
+
'gemini-cli' = @($SkillAgentRoots['gemini-cli'])
|
|
105
|
+
cursor = @($SkillAgentRoots['cursor'])
|
|
106
|
+
antigravity = @($SkillAgentRoots['antigravity'])
|
|
107
|
+
codex = @($SkillAgentRoots['codex'], (Join-Path $HOME ".agents\skills"))
|
|
108
|
+
opencode = @($SkillAgentRoots['opencode'])
|
|
109
|
+
}
|
|
110
|
+
|
|
59
111
|
# ------------------------------------------------------------------------------
|
|
60
112
|
# Logging (human + JSONL)
|
|
61
113
|
# ------------------------------------------------------------------------------
|
|
@@ -96,13 +148,32 @@ function Update-SessionPath {
|
|
|
96
148
|
|
|
97
149
|
# Run a step; $Optional means failures are logged as WARN and swallowed.
|
|
98
150
|
function Invoke-Step {
|
|
99
|
-
param(
|
|
151
|
+
param(
|
|
152
|
+
[string]$Phase,
|
|
153
|
+
[scriptblock]$Action,
|
|
154
|
+
[switch]$Optional,
|
|
155
|
+
[int[]]$ExpectedExitCodes = @()
|
|
156
|
+
)
|
|
100
157
|
Write-Log INFO $Phase "step_start" "Running step"
|
|
101
158
|
try {
|
|
159
|
+
$global:LASTEXITCODE = 0
|
|
102
160
|
& $Action 2>&1 | Tee-Object -FilePath $HumanLog -Append | Out-Host
|
|
161
|
+
$nativeExitCode = $global:LASTEXITCODE
|
|
162
|
+
if ($nativeExitCode -ne 0) {
|
|
163
|
+
if ($ExpectedExitCodes -contains $nativeExitCode) {
|
|
164
|
+
Write-Log INFO $Phase "step_expected" "Step returned expected exit code $nativeExitCode" $nativeExitCode
|
|
165
|
+
return $null
|
|
166
|
+
}
|
|
167
|
+
throw "Native command exited with code $nativeExitCode"
|
|
168
|
+
}
|
|
103
169
|
Write-Log INFO $Phase "step_ok" "Step completed"
|
|
104
170
|
return $true
|
|
105
171
|
} catch {
|
|
172
|
+
$nativeExitCode = $global:LASTEXITCODE
|
|
173
|
+
if ($ExpectedExitCodes -contains $nativeExitCode) {
|
|
174
|
+
Write-Log INFO $Phase "step_expected" "Step returned expected exit code $nativeExitCode" $nativeExitCode
|
|
175
|
+
return $null
|
|
176
|
+
}
|
|
106
177
|
if ($Optional) {
|
|
107
178
|
Write-Log WARN $Phase "step_failed_optional" "$($_.Exception.Message); continuing" 1
|
|
108
179
|
return $false
|
|
@@ -112,19 +183,49 @@ function Invoke-Step {
|
|
|
112
183
|
}
|
|
113
184
|
}
|
|
114
185
|
|
|
115
|
-
function
|
|
186
|
+
function Test-WingetInstalled {
|
|
116
187
|
param([string]$Id, [string]$Phase)
|
|
188
|
+
$probe = Join-Path ([IO.Path]::GetTempPath()) ("setup-ai-winget-" + [guid]::NewGuid().ToString("N") + ".log")
|
|
189
|
+
$wingetNoApplicationsFoundExitCode = -1978335212 # 0x8A150014: package is not installed.
|
|
190
|
+
try {
|
|
191
|
+
# Keep the query inside Invoke-Step so its exit status and output are
|
|
192
|
+
# logged; a failed probe is treated as "not installed" and followed
|
|
193
|
+
# by the mandatory install step.
|
|
194
|
+
$listed = Invoke-Step -Phase $Phase -Optional -ExpectedExitCodes $wingetNoApplicationsFoundExitCode -Action {
|
|
195
|
+
winget list --id $Id -e | Out-File -LiteralPath $probe -Encoding utf8
|
|
196
|
+
}
|
|
197
|
+
if ($null -eq $listed) {
|
|
198
|
+
Write-Log INFO $Phase "not_installed" "$Id is not installed; will install"
|
|
199
|
+
return $false
|
|
200
|
+
}
|
|
201
|
+
return ($listed -and [bool](Select-String -Path $probe -SimpleMatch $Id -Quiet))
|
|
202
|
+
} finally {
|
|
203
|
+
if (Test-Path $probe) {
|
|
204
|
+
try {
|
|
205
|
+
Remove-Item -LiteralPath $probe -Force -ErrorAction Stop
|
|
206
|
+
} catch {
|
|
207
|
+
Write-Log WARN $Phase "cleanup_failed" "Could not remove temporary winget probe: $($_.Exception.Message)"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function Install-Winget {
|
|
214
|
+
param([string]$Id, [string]$Phase, [switch]$Upgrade)
|
|
117
215
|
if (-not (Test-Cmd winget)) {
|
|
118
|
-
|
|
119
|
-
return
|
|
216
|
+
throw "winget not available; cannot install '$Id'"
|
|
120
217
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
218
|
+
if (Test-WingetInstalled -Id $Id -Phase $Phase) {
|
|
219
|
+
if (-not $Upgrade) {
|
|
220
|
+
Write-Log INFO $Phase "already_present" "$Id already installed"
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
Invoke-Step -Phase $Phase -Action {
|
|
224
|
+
winget upgrade -e --id $Id --accept-package-agreements --accept-source-agreements --silent
|
|
225
|
+
}
|
|
125
226
|
return
|
|
126
227
|
}
|
|
127
|
-
Invoke-Step -Phase $Phase -
|
|
228
|
+
Invoke-Step -Phase $Phase -Action {
|
|
128
229
|
winget install -e --id $Id --accept-package-agreements --accept-source-agreements --silent
|
|
129
230
|
}
|
|
130
231
|
}
|
|
@@ -132,35 +233,101 @@ function Install-Winget {
|
|
|
132
233
|
function Invoke-RemoteScript {
|
|
133
234
|
param([string]$Url, [string]$Phase)
|
|
134
235
|
# Mirrors the vendor's documented `irm <url> | iex`, but logged.
|
|
135
|
-
Invoke-Step -Phase $Phase -
|
|
236
|
+
Invoke-Step -Phase $Phase -Action {
|
|
136
237
|
$script = Invoke-RestMethod -Uri $Url -UseBasicParsing
|
|
137
238
|
Invoke-Expression $script
|
|
138
239
|
}
|
|
139
240
|
}
|
|
140
241
|
|
|
242
|
+
function Test-NodeMinimum {
|
|
243
|
+
if (-not (Test-Cmd node)) { return $false }
|
|
244
|
+
$versionText = ""
|
|
245
|
+
$ok = Invoke-Step -Phase "node" -Optional -Action {
|
|
246
|
+
$script:SetupAiNodeVersionProbe = (node --version).Trim()
|
|
247
|
+
}
|
|
248
|
+
if (-not $ok) { return $false }
|
|
249
|
+
$versionText = $script:SetupAiNodeVersionProbe
|
|
250
|
+
if ($versionText -notmatch '^v?(\d+)\.(\d+)') { return $false }
|
|
251
|
+
$major = [int]$Matches[1]
|
|
252
|
+
$minor = [int]$Matches[2]
|
|
253
|
+
return (($major -gt 22) -or (($major -eq 22) -and ($minor -ge 19)))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function Assert-NodeMinimum {
|
|
257
|
+
if (-not (Test-Cmd node)) {
|
|
258
|
+
throw "Node.js is required; install Node.js 22.19 or newer"
|
|
259
|
+
}
|
|
260
|
+
$versionText = ""
|
|
261
|
+
Invoke-Step -Phase "node" -Action {
|
|
262
|
+
$script:SetupAiNodeVersion = (node --version).Trim()
|
|
263
|
+
}
|
|
264
|
+
$versionText = $script:SetupAiNodeVersion
|
|
265
|
+
if ($versionText -notmatch '^v?(\d+)\.(\d+)') {
|
|
266
|
+
throw "Could not parse Node.js version '$versionText'"
|
|
267
|
+
}
|
|
268
|
+
$major = [int]$Matches[1]
|
|
269
|
+
$minor = [int]$Matches[2]
|
|
270
|
+
if (($major -lt 22) -or (($major -eq 22) -and ($minor -lt 19))) {
|
|
271
|
+
throw "Node.js $versionText is too old; pi-extensible-workflows needs >= 22.19"
|
|
272
|
+
}
|
|
273
|
+
Write-Log INFO "node" "runtime_validated" "Node.js version satisfies workflow requirement" 0 "version=$versionText;minimum=22.19"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function Get-ReportCommandValue {
|
|
277
|
+
param([string]$Command, [string[]]$Arguments)
|
|
278
|
+
$script:SetupAiReportValue = ""
|
|
279
|
+
$ok = Invoke-Step -Phase "report" -Optional -Action {
|
|
280
|
+
$script:SetupAiReportValue = (& $Command @Arguments).Trim()
|
|
281
|
+
}
|
|
282
|
+
if ($ok -and $script:SetupAiReportValue) { return $script:SetupAiReportValue }
|
|
283
|
+
return "n/a"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function Get-TargetSkillAgents {
|
|
287
|
+
$found = @()
|
|
288
|
+
foreach ($agent in $SkillAgentNames) {
|
|
289
|
+
if (Test-Path $SkillAgentConfigDirs[$agent]) { $found += $agent }
|
|
290
|
+
}
|
|
291
|
+
if ($found.Count -eq 0) { return @('pi') }
|
|
292
|
+
return $found
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function Assert-SkillInstalledForAgents {
|
|
296
|
+
param([string]$Phase, [string]$Skill, [string[]]$Agents)
|
|
297
|
+
foreach ($agent in $Agents) {
|
|
298
|
+
$checked = @($SkillAgentCandidateRoots[$agent] | ForEach-Object { Join-Path (Join-Path $_ $Skill) "SKILL.md" })
|
|
299
|
+
if (-not ($checked | Where-Object { Test-Path $_ })) {
|
|
300
|
+
throw "$Skill SKILL.md missing for targeted agent '$agent' (checked: $($checked -join ', '))"
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
Write-Log INFO $Phase "skill_verified" "$Skill verified for every targeted agent" 0 "agents=$($Agents -join ',')"
|
|
304
|
+
}
|
|
305
|
+
|
|
141
306
|
# ==============================================================================
|
|
142
307
|
# Module registry
|
|
143
308
|
# ==============================================================================
|
|
144
309
|
|
|
145
|
-
$ModuleOrder = @('base','node','bun','pi','go','ee','pi-workflows','herdr','gentle-ai','
|
|
310
|
+
$ModuleOrder = @('base','node','bun','pi','go','dotenv','ee','skills','pi-workflows','herdr','gentle-ai','codex','antigravity','opencode','cockpit','rotator')
|
|
146
311
|
|
|
147
312
|
$ModuleDesc = [ordered]@{
|
|
148
|
-
'base' = '
|
|
149
|
-
'node' = 'Node.js
|
|
150
|
-
'bun' = 'Bun runtime
|
|
151
|
-
'pi' = 'pi.dev coding agent CLI
|
|
152
|
-
'go' = 'Go toolchain
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'
|
|
162
|
-
|
|
163
|
-
|
|
313
|
+
'base' = 'System packages (build tools, git, gh, python, neovim, jq, imagemagick, go)'
|
|
314
|
+
'node' = 'Node.js v22 + npm@latest (nvm on Unix, winget on Windows)'
|
|
315
|
+
'bun' = 'Bun runtime'
|
|
316
|
+
'pi' = 'pi.dev coding agent CLI'
|
|
317
|
+
'go' = 'Go toolchain'
|
|
318
|
+
'dotenv' = 'vekexasia/dotenv dotfiles (Linux only: clones + runs setup_env.sh)'
|
|
319
|
+
'ee' = 'Engineering Excellence skill (npx skills add, all detected agents)'
|
|
320
|
+
'skills' = 'Upstream agent skills (herdr, grilling, research, typescript-advanced, show-me, ...) via npx skills add'
|
|
321
|
+
'pi-workflows' = 'pi-extensible-workflows (fix module resolution for pi extensions)'
|
|
322
|
+
'herdr' = 'herdr terminal multiplexer'
|
|
323
|
+
'gentle-ai' = 'gentle-ai / gga ecosystem configurator (per-agent select + MCP) + gentle-pi'
|
|
324
|
+
'codex' = 'OpenAI Codex CLI'
|
|
325
|
+
'antigravity' = 'Google Antigravity CLI (agy)'
|
|
326
|
+
'opencode' = 'opencode agent CLI (opencode-ai)'
|
|
327
|
+
'cockpit' = 'cockpit-tools desktop GUI app (optional, CC BY-NC-SA)'
|
|
328
|
+
'rotator' = 'tuxevil-rotator multi-account Gemini/Antigravity gateway (optional, opt-in)'
|
|
329
|
+
}
|
|
330
|
+
$ModuleOptional = @{ 'cockpit' = $true; 'rotator' = $true }
|
|
164
331
|
|
|
165
332
|
# ==============================================================================
|
|
166
333
|
# Modules
|
|
@@ -172,168 +339,323 @@ function Mod-Base {
|
|
|
172
339
|
Install-Winget -Id "GitHub.cli" -Phase "base"
|
|
173
340
|
Install-Winget -Id "Python.Python.3.12" -Phase "base"
|
|
174
341
|
Install-Winget -Id "Neovim.Neovim" -Phase "base"
|
|
342
|
+
# Parity with the advertised description and the Bash base module.
|
|
343
|
+
Install-Winget -Id "jqlang.jq" -Phase "base"
|
|
344
|
+
Install-Winget -Id "ImageMagick.ImageMagick" -Phase "base"
|
|
345
|
+
Install-Winget -Id "GoLang.Go" -Phase "base"
|
|
346
|
+
# Build tools (parity with build-essential): VS Build Tools + C++ workload.
|
|
347
|
+
if (-not (Test-Cmd winget)) { throw "winget not available; cannot install build tools" }
|
|
348
|
+
$vsId = "Microsoft.VisualStudio.2022.BuildTools"
|
|
349
|
+
$vsWhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
|
|
350
|
+
$workloadPresent = $false
|
|
351
|
+
if (Test-Path $vsWhere) {
|
|
352
|
+
$script:SetupAiVsInstallPath = ""
|
|
353
|
+
$probeOk = Invoke-Step -Phase "base" -Optional -Action {
|
|
354
|
+
$script:SetupAiVsInstallPath = (& $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath).Trim()
|
|
355
|
+
}
|
|
356
|
+
$workloadPresent = $probeOk -and [bool]$script:SetupAiVsInstallPath
|
|
357
|
+
}
|
|
358
|
+
if (-not $workloadPresent) {
|
|
359
|
+
Invoke-Step -Phase "base" -Action {
|
|
360
|
+
winget install -e --id $vsId --force --accept-package-agreements --accept-source-agreements `
|
|
361
|
+
--override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
Write-Log INFO "base" "already_present" "Visual Studio C++ workload already installed"
|
|
365
|
+
}
|
|
366
|
+
if (-not (Test-Path $vsWhere)) {
|
|
367
|
+
throw "Visual Studio Installer vswhere.exe not found; cannot verify C++ workload"
|
|
368
|
+
}
|
|
369
|
+
Invoke-Step -Phase "base" -Action {
|
|
370
|
+
$script:SetupAiVsInstallPath = (& $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath).Trim()
|
|
371
|
+
}
|
|
372
|
+
if (-not $script:SetupAiVsInstallPath) {
|
|
373
|
+
throw "Visual Studio C++ workload missing after Build Tools installation"
|
|
374
|
+
}
|
|
175
375
|
Update-SessionPath
|
|
176
376
|
}
|
|
177
377
|
|
|
178
378
|
function Mod-Node {
|
|
179
379
|
Write-Log INFO "node" "start" "Node.js"
|
|
380
|
+
# Upgrade only when the current runtime is below the workflow minimum; a
|
|
381
|
+
# current WinGet package otherwise returns a non-zero "no update" status.
|
|
382
|
+
if (Test-NodeMinimum) {
|
|
383
|
+
Install-Winget -Id "OpenJS.NodeJS.22" -Phase "node"
|
|
384
|
+
} else {
|
|
385
|
+
Install-Winget -Id "OpenJS.NodeJS.22" -Phase "node" -Upgrade
|
|
386
|
+
}
|
|
387
|
+
Update-SessionPath
|
|
388
|
+
# winget's PATH refresh can still lag; add the default install dir directly.
|
|
389
|
+
$nodeDir = Join-Path $env:ProgramFiles "nodejs"
|
|
390
|
+
if ((-not (Test-Cmd node)) -and (Test-Path (Join-Path $nodeDir "node.exe"))) {
|
|
391
|
+
$env:Path = "$nodeDir;$env:Path"
|
|
392
|
+
}
|
|
180
393
|
if (-not (Test-Cmd node)) {
|
|
181
|
-
|
|
182
|
-
Update-SessionPath
|
|
183
|
-
# winget's PATH refresh can still lag; add the default install dir directly.
|
|
184
|
-
$nodeDir = Join-Path $env:ProgramFiles "nodejs"
|
|
185
|
-
if ((-not (Test-Cmd node)) -and (Test-Path (Join-Path $nodeDir "node.exe"))) {
|
|
186
|
-
$env:Path = "$nodeDir;$env:Path"
|
|
187
|
-
}
|
|
394
|
+
throw "node not found on PATH after install"
|
|
188
395
|
}
|
|
189
|
-
if (Test-Cmd npm) {
|
|
190
|
-
|
|
191
|
-
} else {
|
|
192
|
-
Write-Log WARN "node" "unresolved" "node/npm not on PATH after install — open a NEW terminal and re-run (node-dependent modules will be skipped)"
|
|
396
|
+
if (-not (Test-Cmd npm)) {
|
|
397
|
+
throw "npm not found on PATH after Node.js install"
|
|
193
398
|
}
|
|
399
|
+
Invoke-Step -Phase "node" -Action { npm install -g npm@latest }
|
|
400
|
+
Assert-NodeMinimum
|
|
194
401
|
}
|
|
195
402
|
|
|
196
403
|
function Mod-Bun {
|
|
197
404
|
Write-Log INFO "bun" "start" "Bun"
|
|
198
|
-
if (Test-Cmd bun) {
|
|
405
|
+
if (Test-Cmd bun) {
|
|
406
|
+
Invoke-Step -Phase "bun" -Action { bun --version }
|
|
407
|
+
Write-Log INFO "bun" "already_present" "bun already installed"
|
|
408
|
+
return
|
|
409
|
+
}
|
|
199
410
|
Invoke-RemoteScript -Url "https://bun.sh/install.ps1" -Phase "bun"
|
|
411
|
+
if (Test-Cmd bun) {
|
|
412
|
+
Invoke-Step -Phase "bun" -Action { bun --version }
|
|
413
|
+
Write-Log INFO "bun" "installed" "bun available after remote installer"
|
|
414
|
+
} else {
|
|
415
|
+
Write-Log ERROR "bun" "install_missing" "bun not found on PATH after remote installer"
|
|
416
|
+
throw "bun not found on PATH after remote installer"
|
|
417
|
+
}
|
|
200
418
|
}
|
|
201
419
|
|
|
202
420
|
function Mod-Pi {
|
|
203
421
|
Write-Log INFO "pi" "start" "pi.dev CLI"
|
|
204
|
-
|
|
205
|
-
Invoke-RemoteScript -Url "https://pi.dev/install.ps1" -Phase "pi"
|
|
422
|
+
# Establish the same extension/skill roots as Bash even when pi is already installed.
|
|
206
423
|
New-Item -ItemType Directory -Force -Path $PiExtDir | Out-Null
|
|
207
424
|
New-Item -ItemType Directory -Force -Path (Join-Path $HOME ".pi\agent\skills") | Out-Null
|
|
425
|
+
if (Test-Cmd pi) { Write-Log INFO "pi" "already_present" "pi already installed"; return }
|
|
426
|
+
Invoke-RemoteScript -Url "https://pi.dev/install.ps1" -Phase "pi"
|
|
427
|
+
if (Test-Cmd pi) {
|
|
428
|
+
Write-Log INFO "pi" "installed" "pi available after remote installer"
|
|
429
|
+
} else {
|
|
430
|
+
Write-Log ERROR "pi" "install_missing" "pi not found on PATH after remote installer"
|
|
431
|
+
throw "pi not found on PATH after remote installer"
|
|
432
|
+
}
|
|
208
433
|
}
|
|
209
434
|
|
|
210
435
|
function Mod-Go {
|
|
211
436
|
Write-Log INFO "go" "start" "Go toolchain"
|
|
212
|
-
if (Test-Cmd go) {
|
|
437
|
+
if (Test-Cmd go) {
|
|
438
|
+
Invoke-Step -Phase "go" -Action { go version }
|
|
439
|
+
Write-Log INFO "go" "already_present" "Go already installed"
|
|
440
|
+
return
|
|
441
|
+
}
|
|
213
442
|
Install-Winget -Id "GoLang.Go" -Phase "go"
|
|
443
|
+
Update-SessionPath
|
|
444
|
+
if (Test-Cmd go) {
|
|
445
|
+
Write-Log INFO "go" "installed" "Go available after PATH refresh"
|
|
446
|
+
} else {
|
|
447
|
+
throw "Go not on PATH after install"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function Mod-Dotenv {
|
|
452
|
+
Write-Log WARN "dotenv" "skipped_non_linux" "dotenv/setup_env.sh targets Linux package managers; skipped on Windows"
|
|
214
453
|
}
|
|
215
454
|
|
|
216
455
|
function Mod-Ee {
|
|
217
456
|
Write-Log INFO "ee" "start" "Engineering Excellence"
|
|
218
|
-
if (-not (Test-Cmd npx)) {
|
|
219
|
-
|
|
220
|
-
$agents
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (Test-Path (Join-Path $HOME $agents[$a])) {
|
|
224
|
-
Invoke-Step -Phase "ee" -Optional -Action {
|
|
225
|
-
npx --yes skills@latest add $EE_Slug --skill $EE_Skill --global --agent $a --copy --yes
|
|
226
|
-
}
|
|
227
|
-
$any = $true
|
|
457
|
+
if (-not (Test-Cmd npx)) { throw "npx not found; ee cannot be installed (install the node module first)" }
|
|
458
|
+
$agents = Get-TargetSkillAgents
|
|
459
|
+
foreach ($a in $agents) {
|
|
460
|
+
Invoke-Step -Phase "ee" -Action {
|
|
461
|
+
npx --yes skills@latest add $EE_Slug --skill $EE_Skill --global --agent $a --copy --yes
|
|
228
462
|
}
|
|
229
463
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
464
|
+
Assert-SkillInstalledForAgents -Phase "ee" -Skill $EE_Skill -Agents $agents
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# Installs vekexasia/dotenv's skill stack on every OS via `npx skills add`.
|
|
468
|
+
# On Linux the dotenv module may already install these; skills add --copy is
|
|
469
|
+
# idempotent, so a re-run is safe.
|
|
470
|
+
function Mod-Skills {
|
|
471
|
+
Write-Log INFO "skills" "start" "Agent skills (upstream stack)"
|
|
472
|
+
if (-not (Test-Cmd npx)) { throw "npx not found; skills cannot be installed (install the node module first)" }
|
|
473
|
+
$agents = Get-TargetSkillAgents
|
|
474
|
+
foreach ($entry in $UpstreamSkillSources) {
|
|
475
|
+
$src = $entry.Source
|
|
476
|
+
$skills = $entry.Skills
|
|
477
|
+
foreach ($a in $agents) {
|
|
478
|
+
Invoke-Step -Phase "skills" -Action {
|
|
479
|
+
npx --yes skills@latest add $src --skill $skills --global --agent $a --copy --yes
|
|
480
|
+
}
|
|
233
481
|
}
|
|
234
482
|
}
|
|
235
|
-
|
|
236
|
-
|
|
483
|
+
foreach ($skill in $UpstreamSkillNames) {
|
|
484
|
+
Assert-SkillInstalledForAgents -Phase "skills" -Skill $skill -Agents $agents
|
|
237
485
|
}
|
|
238
486
|
}
|
|
239
487
|
|
|
240
488
|
function Mod-PiWorkflows {
|
|
241
489
|
Write-Log INFO "pi-workflows" "start" "pi-extensible-workflows"
|
|
242
|
-
if (-not (Test-Cmd pi)) {
|
|
490
|
+
if (-not (Test-Cmd pi)) { throw "pi not found; pi-workflows cannot be installed" }
|
|
491
|
+
if (-not (Test-Cmd npm)) { throw "npm not found; pi-workflows cannot be installed" }
|
|
492
|
+
Assert-NodeMinimum
|
|
243
493
|
$ver = ""
|
|
244
|
-
try {
|
|
245
|
-
|
|
494
|
+
try {
|
|
495
|
+
Invoke-Step -Phase "pi-workflows" -Action {
|
|
496
|
+
$script:SetupAiWorkflowVersion = (npm view pi-extensible-workflows version).Trim()
|
|
497
|
+
}
|
|
498
|
+
$ver = $script:SetupAiWorkflowVersion
|
|
499
|
+
} catch {
|
|
500
|
+
throw "Could not resolve pi-extensible-workflows version: $($_.Exception.Message)"
|
|
501
|
+
}
|
|
502
|
+
if (-not $ver) { throw "Could not resolve pi-extensible-workflows version" }
|
|
246
503
|
Write-Log INFO "pi-workflows" "version" "Version $ver"
|
|
247
|
-
Invoke-Step -Phase "pi-workflows" -
|
|
504
|
+
Invoke-Step -Phase "pi-workflows" -Action { pi install "npm:pi-extensible-workflows@$ver" }
|
|
248
505
|
New-Item -ItemType Directory -Force -Path $PiExtDir | Out-Null
|
|
249
506
|
Set-Content -Path (Join-Path $PiExtDir ".npmrc") -Value "ignore-scripts=false"
|
|
507
|
+
# Mark this dir as an npm project root so `npm install` lands HERE and cannot
|
|
508
|
+
# walk up into an ancestor project (mirrors setup-ai.sh).
|
|
509
|
+
$pkgJson = Join-Path $PiExtDir "package.json"
|
|
510
|
+
if (-not (Test-Path $pkgJson)) {
|
|
511
|
+
Set-Content -Path $pkgJson -Value '{"name":"pi-extensions","private":true}'
|
|
512
|
+
}
|
|
250
513
|
Push-Location $PiExtDir
|
|
251
514
|
try {
|
|
252
|
-
Invoke-Step -Phase "pi-workflows" -
|
|
515
|
+
Invoke-Step -Phase "pi-workflows" -Action {
|
|
253
516
|
npm install --save-exact --no-audit --no-fund "pi-extensible-workflows@$ver"
|
|
254
517
|
}
|
|
255
|
-
Invoke-Step -Phase "pi-workflows" -Optional -Action {
|
|
256
|
-
node -e "console.log(require.resolve('pi-extensible-workflows',{paths:[process.cwd()]}))"
|
|
257
|
-
}
|
|
258
518
|
} finally { Pop-Location }
|
|
519
|
+
|
|
520
|
+
# Verify the version that actually landed in THIS dir by reading its local
|
|
521
|
+
# package.json directly. Do NOT use require.resolve: it ascends the tree and
|
|
522
|
+
# can resolve a shadowing ancestor copy (false version_mismatch).
|
|
523
|
+
$localPkg = Join-Path $PiExtDir "node_modules/pi-extensible-workflows/package.json"
|
|
524
|
+
if (-not (Test-Path $localPkg)) {
|
|
525
|
+
Write-Log ERROR "pi-workflows-node" "install_missing" "pi-extensible-workflows not installed in extensions dir ($localPkg)"
|
|
526
|
+
throw "pi-extensible-workflows not installed in extensions dir ($localPkg)"
|
|
527
|
+
} else {
|
|
528
|
+
$installed = (Get-Content -Raw $localPkg | ConvertFrom-Json).version
|
|
529
|
+
if ($installed -ne $ver) {
|
|
530
|
+
Write-Log ERROR "pi-workflows-node" "version_mismatch" "Installed version mismatch (expected=$ver actual=$installed)"
|
|
531
|
+
throw "Installed pi-extensible-workflows version mismatch (expected=$ver actual=$installed)"
|
|
532
|
+
} else {
|
|
533
|
+
Write-Log INFO "pi-workflows-node" "module_resolved" "Installed $installed in $localPkg"
|
|
534
|
+
}
|
|
535
|
+
}
|
|
259
536
|
}
|
|
260
537
|
|
|
261
538
|
function Mod-Herdr {
|
|
262
539
|
Write-Log INFO "herdr" "start" "herdr"
|
|
263
|
-
if (Test-Cmd herdr) {
|
|
540
|
+
if (Test-Cmd herdr) {
|
|
541
|
+
Invoke-Step -Phase "herdr" -Action { herdr --version }
|
|
542
|
+
Write-Log INFO "herdr" "already_present" "herdr already installed"
|
|
543
|
+
return
|
|
544
|
+
}
|
|
264
545
|
Invoke-RemoteScript -Url "https://herdr.dev/install.ps1" -Phase "herdr"
|
|
546
|
+
if (Test-Cmd herdr) {
|
|
547
|
+
Write-Log INFO "herdr" "installed" "herdr available after remote installer"
|
|
548
|
+
} else {
|
|
549
|
+
Write-Log ERROR "herdr" "install_missing" "herdr not found on PATH after remote installer"
|
|
550
|
+
throw "herdr not found on PATH after remote installer"
|
|
551
|
+
}
|
|
265
552
|
}
|
|
266
553
|
|
|
554
|
+
# Reinstates the Gentle AI ecosystem configurator (sibling of mod_gentle_ai in
|
|
555
|
+
# setup-ai.sh). `gentle-ai install` is the per-agent/per-IDE selector that also
|
|
556
|
+
# wires each selected agent's MCP servers, so tools appear under /mcp. The
|
|
557
|
+
# configurator is a CORE step: it always runs (interactively with a console, or
|
|
558
|
+
# non-interactively over the detected agents in CI/pipes) and a failure fails
|
|
559
|
+
# the module. For pi we additionally guarantee + verify the first-class
|
|
560
|
+
# gentle-pi harness and pi-mcp-adapter. Idempotent: safe to re-run.
|
|
267
561
|
function Mod-GentleAi {
|
|
268
562
|
Write-Log INFO "gentle-ai" "start" "gentle-ai"
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
563
|
+
|
|
564
|
+
# The per-agent selector and the pi harness both require the gentle-ai CLI
|
|
565
|
+
# itself; a legacy standalone gga is NOT enough, so install whenever the
|
|
566
|
+
# gentle-ai CLI is missing even if an old gga is on PATH.
|
|
567
|
+
if (-not (Test-Cmd gentle-ai)) {
|
|
568
|
+
# Vendor's official Windows method (installs to %LOCALAPPDATA%\gentle-ai\bin).
|
|
569
|
+
Invoke-RemoteScript -Url "https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.ps1" -Phase "gentle-ai"
|
|
570
|
+
Update-SessionPath
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (-not (Test-Cmd gentle-ai)) {
|
|
574
|
+
Write-Log ERROR "gentle-ai" "install_missing" "gentle-ai CLI not found on PATH after remote installer"
|
|
575
|
+
throw "gentle-ai CLI not found on PATH after remote installer"
|
|
576
|
+
}
|
|
577
|
+
Invoke-Step -Phase "gentle-ai" -Action { & gentle-ai --version }
|
|
578
|
+
|
|
579
|
+
# Detect the agents/IDEs present on this machine (same mapping as Mod-Ee).
|
|
580
|
+
$detectedAgents = Get-TargetSkillAgents
|
|
581
|
+
|
|
582
|
+
# Per-agent / per-IDE selection + MCP wiring, owned by gentle-ai. This is a
|
|
583
|
+
# core step and must actually run: with a real console we launch the
|
|
584
|
+
# interactive selector (run directly — Invoke-Step pipes output and would
|
|
585
|
+
# hide the prompts); otherwise we run it non-interactively over the detected
|
|
586
|
+
# agents so CI/pipes never hang. A failure fails the module.
|
|
587
|
+
if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected -and -not [Console]::IsOutputRedirected) {
|
|
588
|
+
Write-Log INFO "gentle-ai" "configurator_start" "Launching gentle-ai install (choose agents/IDEs + MCP)"
|
|
589
|
+
& gentle-ai install --scope global
|
|
590
|
+
if ($LASTEXITCODE -ne 0) {
|
|
591
|
+
Write-Log ERROR "gentle-ai" "configurator_failed" "gentle-ai install exited with code $LASTEXITCODE"
|
|
592
|
+
throw "gentle-ai install exited with code $LASTEXITCODE"
|
|
276
593
|
}
|
|
594
|
+
} else {
|
|
595
|
+
$agentsCsv = ($detectedAgents -join ',')
|
|
596
|
+
Write-Log INFO "gentle-ai" "configurator_noninteractive" "No console; installing gentle-ai for detected agents" 0 "agents=$agentsCsv"
|
|
597
|
+
Invoke-Step -Phase "gentle-ai" -Action { & gentle-ai install --scope global --agents $agentsCsv }
|
|
277
598
|
}
|
|
599
|
+
Write-Log INFO "gentle-ai" "configurator_done" "gentle-ai install completed"
|
|
600
|
+
|
|
601
|
+
# Guarantee pi reads gentle-ai in its MCP list (/mcp): install the first-class
|
|
602
|
+
# gentle-pi harness + pi-mcp-adapter, then verify the exact target file.
|
|
278
603
|
if (Test-Cmd pi) {
|
|
279
|
-
Invoke-Step -Phase "gentle-ai" -
|
|
280
|
-
Invoke-Step -Phase "gentle-ai" -
|
|
281
|
-
|
|
604
|
+
Invoke-Step -Phase "gentle-ai" -Action { pi install npm:gentle-pi }
|
|
605
|
+
Invoke-Step -Phase "gentle-ai" -Action { pi install npm:pi-mcp-adapter }
|
|
606
|
+
$piSettings = Join-Path $HOME ".pi\agent\settings.json"
|
|
607
|
+
$piSettingsRaw = if (Test-Path $piSettings) { Get-Content -Raw $piSettings } else { "" }
|
|
608
|
+
if (($piSettingsRaw -match 'npm:gentle-pi') -and ($piSettingsRaw -match 'npm:pi-mcp-adapter')) {
|
|
609
|
+
Write-Log INFO "gentle-ai" "pi_enabled" "gentle-pi + pi-mcp-adapter registered in pi (verify: /mcp, /gentle-ai:status)"
|
|
610
|
+
} else {
|
|
611
|
+
Write-Log ERROR "gentle-ai" "pi_enable_failed" "gentle-pi and/or pi-mcp-adapter not present in pi settings after install ($piSettings)"
|
|
612
|
+
throw "gentle-pi and/or pi-mcp-adapter not present in pi settings after install"
|
|
613
|
+
}
|
|
282
614
|
}
|
|
615
|
+
|
|
283
616
|
@"
|
|
284
617
|
gentle-ai next steps (run yourself, per project):
|
|
285
618
|
1) Set your API keys
|
|
286
619
|
2) Run your selected agent
|
|
287
|
-
3) Try: /sdd-new my-feature (in pi: /gentle-ai:status, /gentleman:models)
|
|
620
|
+
3) Try: /sdd-new my-feature (in pi: /gentle-ai:status, /gentleman:models, /mcp)
|
|
288
621
|
GGA (per project): gga init then gga install
|
|
289
622
|
"@ | Tee-Object -FilePath $HumanLog -Append | Out-Host
|
|
290
623
|
}
|
|
291
624
|
|
|
292
|
-
function Mod-Engram {
|
|
293
|
-
Write-Log INFO "engram" "start" "Engram memory (pi)"
|
|
294
|
-
# The gentle-engram pi extension auto-starts `engram serve`; the Engram Go
|
|
295
|
-
# binary must be on PATH first, or the extension loads but silently fails.
|
|
296
|
-
if (-not (Test-Cmd engram)) {
|
|
297
|
-
if (Test-Cmd go) {
|
|
298
|
-
Invoke-Step -Phase "engram" -Optional -Action {
|
|
299
|
-
go install github.com/Gentleman-Programming/engram/cmd/engram@latest
|
|
300
|
-
}
|
|
301
|
-
$goBin = Join-Path $HOME "go\bin"
|
|
302
|
-
if ((Test-Path (Join-Path $goBin "engram.exe")) -and ($env:Path -notlike "*$goBin*")) {
|
|
303
|
-
$env:Path = "$goBin;$env:Path"
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
Write-Log WARN "engram" "go_missing" "Go not found; cannot install engram binary (needed on PATH)"
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (-not (Test-Cmd pi)) { Write-Log WARN "engram" "pi_missing" "pi not found; skipped"; return }
|
|
310
|
-
# `pi-engram init` is the single source of truth (adds pinned gentle-engram +
|
|
311
|
-
# wires the Engram MCP server). Do NOT also `pi install npm:gentle-engram` —
|
|
312
|
-
# that adds a second, unversioned entry and duplicates it every run.
|
|
313
|
-
Invoke-Step -Phase "engram" -Optional -Action { pi install npm:pi-mcp-adapter }
|
|
314
|
-
Invoke-Step -Phase "engram" -Optional -Action { npm exec --yes --package gentle-engram@latest -- pi-engram init }
|
|
315
|
-
Write-Log INFO "engram" "enabled" "Engram enabled — RESTART pi, verify: mem_current_project / mem_doctor / 'engram tui'"
|
|
316
|
-
}
|
|
317
|
-
|
|
318
625
|
function Mod-Codex {
|
|
319
626
|
Write-Log INFO "codex" "start" "Codex CLI"
|
|
320
627
|
if (Test-Cmd codex) { Write-Log INFO "codex" "already_present" "codex already installed"; return }
|
|
321
628
|
Invoke-RemoteScript -Url "https://chatgpt.com/codex/install.ps1" -Phase "codex"
|
|
629
|
+
if (Test-Cmd codex) {
|
|
630
|
+
Write-Log INFO "codex" "installed" "codex available after remote installer"
|
|
631
|
+
} else {
|
|
632
|
+
Write-Log ERROR "codex" "install_missing" "codex not found on PATH after remote installer"
|
|
633
|
+
throw "codex not found on PATH after remote installer"
|
|
634
|
+
}
|
|
322
635
|
}
|
|
323
636
|
|
|
324
637
|
function Mod-Antigravity {
|
|
325
638
|
Write-Log INFO "antigravity" "start" "Antigravity CLI"
|
|
326
639
|
if (Test-Cmd agy) { Write-Log INFO "antigravity" "already_present" "agy already installed"; return }
|
|
327
640
|
Invoke-RemoteScript -Url "https://antigravity.google/cli/install.ps1" -Phase "antigravity"
|
|
641
|
+
if (Test-Cmd agy) {
|
|
642
|
+
Write-Log INFO "antigravity" "installed" "agy available after remote installer"
|
|
643
|
+
} else {
|
|
644
|
+
Write-Log ERROR "antigravity" "install_missing" "agy not found on PATH after remote installer"
|
|
645
|
+
throw "agy not found on PATH after remote installer"
|
|
646
|
+
}
|
|
328
647
|
}
|
|
329
648
|
|
|
330
649
|
function Mod-Opencode {
|
|
331
650
|
Write-Log INFO "opencode" "start" "opencode"
|
|
332
651
|
if (Test-Cmd opencode) { Write-Log INFO "opencode" "already_present" "opencode already installed"; return }
|
|
333
|
-
if (Test-Cmd npm) {
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
652
|
+
if (-not (Test-Cmd npm)) {
|
|
653
|
+
throw "npm not found; opencode cannot be installed"
|
|
654
|
+
}
|
|
655
|
+
Invoke-Step -Phase "opencode" -Action { npm install -g opencode-ai }
|
|
656
|
+
if (-not (Test-Cmd opencode)) {
|
|
657
|
+
Write-Log ERROR "opencode" "install_missing" "opencode not found on PATH after npm install"
|
|
658
|
+
throw "opencode not found on PATH after npm install"
|
|
337
659
|
}
|
|
338
660
|
@"
|
|
339
661
|
OpenCode Go (paid) is hosted-model access; after install run: opencode auth login
|
|
@@ -351,19 +673,95 @@ function Mod-Cockpit {
|
|
|
351
673
|
if (-not $asset) { Write-Log WARN "cockpit" "no_msi" "No .msi asset in latest release; download manually"; return }
|
|
352
674
|
$msi = Join-Path $env:TEMP $asset.name
|
|
353
675
|
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $msi
|
|
354
|
-
Invoke-Step -Phase "cockpit" -Optional -Action {
|
|
676
|
+
Invoke-Step -Phase "cockpit" -Optional -Action {
|
|
677
|
+
$process = Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /qb" -Wait -PassThru
|
|
678
|
+
if ($process.ExitCode -ne 0) { throw "msiexec exited with code $($process.ExitCode)" }
|
|
679
|
+
}
|
|
355
680
|
} catch {
|
|
356
681
|
Write-Log WARN "cockpit" "failed" "$($_.Exception.Message)"
|
|
357
682
|
}
|
|
358
683
|
}
|
|
359
684
|
|
|
685
|
+
function Mod-Rotator {
|
|
686
|
+
Write-Log INFO "rotator" "start" "tuxevil-rotator gateway"
|
|
687
|
+
# Detect a desktop cockpit-tools data dir by marker file only; never read tokens.
|
|
688
|
+
$candidates = @(
|
|
689
|
+
(Join-Path $HOME ".antigravity_cockpit")
|
|
690
|
+
(Join-Path $HOME ".local\share\cockpit-tools")
|
|
691
|
+
(Join-Path $HOME ".config\cockpit-tools")
|
|
692
|
+
(Join-Path $HOME ".wizard-ai\cockpit-tools")
|
|
693
|
+
(Join-Path $HOME "Library\Application Support\cockpit-tools")
|
|
694
|
+
)
|
|
695
|
+
if ($env:APPDATA) { $candidates += Join-Path $env:APPDATA "cockpit-tools" }
|
|
696
|
+
if ($env:LOCALAPPDATA) { $candidates += Join-Path $env:LOCALAPPDATA "cockpit-tools" }
|
|
697
|
+
|
|
698
|
+
$cockpitDir = ""
|
|
699
|
+
foreach ($dir in $candidates) {
|
|
700
|
+
if ((Test-Path -LiteralPath (Join-Path $dir "accounts.json") -PathType Leaf) -or
|
|
701
|
+
(Test-Path -LiteralPath (Join-Path $dir "account-token.key") -PathType Leaf)) {
|
|
702
|
+
$cockpitDir = $dir
|
|
703
|
+
break
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if ($cockpitDir) {
|
|
707
|
+
Write-Log INFO "rotator" "cockpit_detected" "cockpit-tools data directory detected" 0 "dir=$cockpitDir"
|
|
708
|
+
} else {
|
|
709
|
+
Write-Log INFO "rotator" "cockpit_absent" "No cockpit-tools data directory detected; the rotator can still use its own accounts" 0
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
# Non-fatal health probe.
|
|
713
|
+
$gw = "http://localhost:51200/v1/models"
|
|
714
|
+
try {
|
|
715
|
+
$response = Invoke-WebRequest -Uri $gw -Headers @{ Authorization = "Bearer tuxevil" } -TimeoutSec 5
|
|
716
|
+
$body = $response.Content
|
|
717
|
+
$count = if ($body) { ([regex]::Matches($body, '"id"')).Count } else { 0 }
|
|
718
|
+
Write-Log INFO "rotator" "gateway_up" "tuxevil-rotator gateway is reachable" 0 "url=$gw;models=$count"
|
|
719
|
+
} catch {
|
|
720
|
+
Write-Log INFO "rotator" "gateway_down" "tuxevil-rotator gateway not reachable; start it with 'tuxevil-rotator start'" 0 "url=$gw"
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
# Install the CLI idempotently. Never runs login/start or writes secrets.
|
|
724
|
+
if (Test-Cmd tuxevil-rotator) {
|
|
725
|
+
Write-Log INFO "rotator" "already_present" "tuxevil-rotator already installed"
|
|
726
|
+
} else {
|
|
727
|
+
if (-not (Test-Cmd npm)) { throw "npm not found; tuxevil-rotator cannot be installed" }
|
|
728
|
+
Invoke-Step -Phase "rotator" -Action { npm install -g tuxevil-rotator }
|
|
729
|
+
if (-not (Test-Cmd tuxevil-rotator)) {
|
|
730
|
+
Write-Log ERROR "rotator" "install_missing" "tuxevil-rotator not found on PATH after npm install"
|
|
731
|
+
throw "tuxevil-rotator not found on PATH after npm install"
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (Test-Cmd pi) {
|
|
735
|
+
Invoke-Step -Phase "rotator" -Action { pi install "github:darkrei08/pi-cockpit-tools-sync" }
|
|
736
|
+
$piSettings = Join-Path $HOME ".pi\agent\settings.json"
|
|
737
|
+
if (-not (Test-Path -LiteralPath $piSettings -PathType Leaf) -or
|
|
738
|
+
-not (Select-String -LiteralPath $piSettings -SimpleMatch "github:darkrei08/pi-cockpit-tools-sync" -Quiet)) {
|
|
739
|
+
Write-Log ERROR "rotator" "pi_extension_missing" "Pi did not register cockpit sync extension" 1 "expected=$piSettings"
|
|
740
|
+
throw "Pi did not register cockpit sync extension"
|
|
741
|
+
}
|
|
742
|
+
Write-Log INFO "rotator" "pi_extension_verified" "Cockpit sync extension registered in Pi" 0 "path=$piSettings"
|
|
743
|
+
} else {
|
|
744
|
+
Write-Log INFO "rotator" "pi_extension_skipped" "pi not found; cockpit sync extension was not installed"
|
|
745
|
+
}
|
|
746
|
+
@"
|
|
747
|
+
tuxevil-rotator installed. To use the multi-account Gemini/Antigravity gateway:
|
|
748
|
+
tuxevil-rotator login # add a Google Antigravity account (repeat to add more)
|
|
749
|
+
tuxevil-rotator import # or bulk-import accounts from a cockpit-tools JSON
|
|
750
|
+
tuxevil-rotator start # start the rotating proxy on http://localhost:51200
|
|
751
|
+
Pi reaches it through the 'tuxevil-rotator' provider configured in your dotenv.
|
|
752
|
+
The cockpit sync extension provides /cockpit-sync, /cockpit-provision, and /cockpit-proxy.
|
|
753
|
+
Login/start are never run by setup-ai and no tokens are read or stored.
|
|
754
|
+
"@ | Tee-Object -FilePath $HumanLog -Append | Out-Host
|
|
755
|
+
}
|
|
756
|
+
|
|
360
757
|
$ModuleFn = @{
|
|
361
758
|
'base' = ${function:Mod-Base}; 'node' = ${function:Mod-Node}; 'bun' = ${function:Mod-Bun}
|
|
362
|
-
'pi' = ${function:Mod-Pi}; 'go' = ${function:Mod-Go}; 'ee' = ${function:Mod-Ee}
|
|
759
|
+
'pi' = ${function:Mod-Pi}; 'go' = ${function:Mod-Go}; 'dotenv' = ${function:Mod-Dotenv}; 'ee' = ${function:Mod-Ee}
|
|
760
|
+
'skills' = ${function:Mod-Skills}
|
|
363
761
|
'pi-workflows' = ${function:Mod-PiWorkflows}; 'herdr' = ${function:Mod-Herdr}
|
|
364
|
-
'gentle-ai' = ${function:Mod-GentleAi}
|
|
762
|
+
'gentle-ai' = ${function:Mod-GentleAi}
|
|
365
763
|
'codex' = ${function:Mod-Codex}; 'antigravity' = ${function:Mod-Antigravity}
|
|
366
|
-
'opencode' = ${function:Mod-Opencode}; 'cockpit' = ${function:Mod-Cockpit}
|
|
764
|
+
'opencode' = ${function:Mod-Opencode}; 'cockpit' = ${function:Mod-Cockpit}; 'rotator' = ${function:Mod-Rotator}
|
|
367
765
|
}
|
|
368
766
|
|
|
369
767
|
# ==============================================================================
|
|
@@ -376,11 +774,11 @@ function Show-List {
|
|
|
376
774
|
$tag = if ($ModuleOptional.ContainsKey($m)) { "optional" } else { "core " }
|
|
377
775
|
"{0,-10} {1,-14} {2}" -f "[$tag]", $m, $ModuleDesc[$m] | Write-Host
|
|
378
776
|
}
|
|
379
|
-
Write-Host "`nUse: -Only
|
|
777
|
+
Write-Host "`nUse: -Only csv | -All | (default = core)"
|
|
380
778
|
}
|
|
381
779
|
|
|
382
780
|
function Show-Help {
|
|
383
|
-
Get-Content $
|
|
781
|
+
Get-Content $ScriptPath | Select-Object -First 30 | ForEach-Object { $_ }
|
|
384
782
|
Show-List
|
|
385
783
|
}
|
|
386
784
|
|
|
@@ -388,13 +786,21 @@ function Resolve-Selection {
|
|
|
388
786
|
$requested = @()
|
|
389
787
|
if ($All) {
|
|
390
788
|
$requested = $ModuleOrder
|
|
391
|
-
} elseif ($
|
|
789
|
+
} elseif ($OnlySpecified) {
|
|
790
|
+
if ([string]::IsNullOrWhiteSpace($Only)) {
|
|
791
|
+
Write-Log ERROR "selection" "invalid_only" "-Only requires a non-empty comma-separated module list." 2
|
|
792
|
+
exit 2
|
|
793
|
+
}
|
|
392
794
|
foreach ($r in ($Only -split ',')) {
|
|
393
795
|
$r = $r.Trim()
|
|
394
796
|
if (-not $r) { continue }
|
|
395
797
|
if (-not $ModuleDesc.Contains($r)) { Write-Error "Unknown module: $r"; exit 2 }
|
|
396
798
|
$requested += $r
|
|
397
799
|
}
|
|
800
|
+
if ($requested.Count -eq 0) {
|
|
801
|
+
Write-Log ERROR "selection" "invalid_only" "-Only requires a non-empty comma-separated module list." 2
|
|
802
|
+
exit 2
|
|
803
|
+
}
|
|
398
804
|
} else {
|
|
399
805
|
$requested = $ModuleOrder | Where-Object { -not $ModuleOptional.ContainsKey($_) }
|
|
400
806
|
}
|
|
@@ -402,6 +808,66 @@ function Resolve-Selection {
|
|
|
402
808
|
return $ModuleOrder | Where-Object { $requested -contains $_ }
|
|
403
809
|
}
|
|
404
810
|
|
|
811
|
+
function Invoke-QualityGates {
|
|
812
|
+
param([string[]]$Selected)
|
|
813
|
+
Write-Log INFO "quality" "start" "Running quality gates"
|
|
814
|
+
|
|
815
|
+
if ($Selected -contains 'node' -or $Selected -contains 'pi-workflows') {
|
|
816
|
+
Assert-NodeMinimum
|
|
817
|
+
}
|
|
818
|
+
if ($Selected -contains 'node') {
|
|
819
|
+
Invoke-Step -Phase "quality" -Action { node --version }
|
|
820
|
+
Invoke-Step -Phase "quality" -Action { npm --version }
|
|
821
|
+
}
|
|
822
|
+
if ($Selected -contains 'bun') {
|
|
823
|
+
if (-not (Test-Cmd bun)) { throw "bun quality gate could not find bun" }
|
|
824
|
+
Invoke-Step -Phase "quality" -Action { bun --version }
|
|
825
|
+
}
|
|
826
|
+
if ($Selected -contains 'pi') {
|
|
827
|
+
if (-not (Test-Cmd pi)) { throw "pi quality gate could not find pi" }
|
|
828
|
+
Invoke-Step -Phase "quality" -Action { pi --no-extensions --version }
|
|
829
|
+
}
|
|
830
|
+
if ($Selected -contains 'pi-workflows') {
|
|
831
|
+
$workflowPkg = Join-Path $PiExtDir "node_modules/pi-extensible-workflows/package.json"
|
|
832
|
+
if (-not (Test-Path $workflowPkg)) {
|
|
833
|
+
throw "pi-extensible-workflows package.json missing from extensions dir ($workflowPkg)"
|
|
834
|
+
}
|
|
835
|
+
$workflowVersion = (Get-Content -Raw $workflowPkg | ConvertFrom-Json).version
|
|
836
|
+
Write-Log INFO "quality" "workflow_package_verified" "Verified pi-extensible-workflows package" 0 "path=$workflowPkg;version=$workflowVersion"
|
|
837
|
+
}
|
|
838
|
+
$targetAgents = Get-TargetSkillAgents
|
|
839
|
+
if ($Selected -contains 'ee') {
|
|
840
|
+
Assert-SkillInstalledForAgents -Phase "quality" -Skill $EE_Skill -Agents $targetAgents
|
|
841
|
+
Write-Log INFO "quality" "ee_gate_passed" "Engineering Excellence verified for every targeted agent"
|
|
842
|
+
}
|
|
843
|
+
if ($Selected -contains 'skills') {
|
|
844
|
+
foreach ($sk in $UpstreamSkillNames) {
|
|
845
|
+
Assert-SkillInstalledForAgents -Phase "quality" -Skill $sk -Agents $targetAgents
|
|
846
|
+
}
|
|
847
|
+
Write-Log INFO "quality" "skills_gate_passed" "All upstream skills verified for every targeted agent"
|
|
848
|
+
}
|
|
849
|
+
if ($Selected -contains 'gentle-ai') {
|
|
850
|
+
# The module requires the gentle-ai CLI specifically; a legacy standalone
|
|
851
|
+
# gga cannot prove the per-agent/MCP configuration ran.
|
|
852
|
+
if (-not (Test-Cmd gentle-ai)) {
|
|
853
|
+
throw "gentle-ai quality gate could not find the gentle-ai CLI on PATH"
|
|
854
|
+
}
|
|
855
|
+
if (Test-Cmd pi) {
|
|
856
|
+
$piSettings = Join-Path $HOME ".pi\agent\settings.json"
|
|
857
|
+
$piSettingsRaw = if (Test-Path $piSettings) { Get-Content -Raw $piSettings } else { "" }
|
|
858
|
+
if (-not ($piSettingsRaw -match 'npm:gentle-pi') -or -not ($piSettingsRaw -match 'npm:pi-mcp-adapter')) {
|
|
859
|
+
throw "gentle-pi and/or pi-mcp-adapter not registered in pi settings ($piSettings)"
|
|
860
|
+
}
|
|
861
|
+
Write-Log INFO "quality" "gentle_ai_gate_passed" "gentle-ai verified (CLI present; gentle-pi + pi-mcp-adapter registered in pi)"
|
|
862
|
+
} else {
|
|
863
|
+
# No pi on PATH: pi MCP wiring is genuinely not applicable, so do not
|
|
864
|
+
# claim it was registered.
|
|
865
|
+
Write-Log INFO "quality" "gentle_ai_gate_passed" "gentle-ai CLI verified; pi not present, so pi MCP wiring was not required"
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
Write-Log INFO "quality" "gates_done" "Quality gates completed for selected modules"
|
|
869
|
+
}
|
|
870
|
+
|
|
405
871
|
# ==============================================================================
|
|
406
872
|
# Main
|
|
407
873
|
# ==============================================================================
|
|
@@ -420,10 +886,29 @@ Write-Log INFO "bootstrap" "modules_selected" "Modules queued" 0 ("modules=" + (
|
|
|
420
886
|
$failed = @()
|
|
421
887
|
foreach ($m in $selected) {
|
|
422
888
|
try { & $ModuleFn[$m] }
|
|
423
|
-
catch {
|
|
889
|
+
catch {
|
|
890
|
+
$failed += $m
|
|
891
|
+
Write-Log ERROR "modules" "module_failed" "Module $m failed: $($_.Exception.Message)" 1
|
|
892
|
+
# Fail-fast: mirror the Bash ERR trap so we never run modules whose
|
|
893
|
+
# ordered prerequisites just failed.
|
|
894
|
+
break
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
# Skip quality gates when a module already failed (Bash aborts before them).
|
|
899
|
+
if (-not $failed) {
|
|
900
|
+
try {
|
|
901
|
+
Invoke-QualityGates -Selected $selected
|
|
902
|
+
} catch {
|
|
903
|
+
$failed += 'quality'
|
|
904
|
+
Write-Log ERROR "quality" "gates_failed" $_.Exception.Message 1
|
|
905
|
+
}
|
|
424
906
|
}
|
|
425
907
|
|
|
426
908
|
# Report
|
|
909
|
+
$reportNode = if (Test-Cmd node) { Get-ReportCommandValue -Command "node" -Arguments @("--version") } else { "n/a" }
|
|
910
|
+
$reportNpm = if (Test-Cmd npm) { Get-ReportCommandValue -Command "npm" -Arguments @("--version") } else { "n/a" }
|
|
911
|
+
$reportGo = if (Test-Cmd go) { Get-ReportCommandValue -Command "go" -Arguments @("version") } else { "n/a" }
|
|
427
912
|
@"
|
|
428
913
|
# AI Dev Suite — Engineering Report (Windows)
|
|
429
914
|
|
|
@@ -433,9 +918,9 @@ foreach ($m in $selected) {
|
|
|
433
918
|
**Failed modules:** $(if ($failed) { $failed -join ' ' } else { 'none' })
|
|
434
919
|
|
|
435
920
|
## Versions
|
|
436
|
-
- Node: $
|
|
437
|
-
- npm: $
|
|
438
|
-
- Go: $
|
|
921
|
+
- Node: $reportNode
|
|
922
|
+
- npm: $reportNpm
|
|
923
|
+
- Go: $reportGo
|
|
439
924
|
- pi: $(if (Test-Cmd pi) { 'installed' } else { 'n/a' })
|
|
440
925
|
|
|
441
926
|
Logs: $HumanLog ; $JsonlLog
|
|
@@ -445,6 +930,12 @@ Write-Host "`n============================================================" -For
|
|
|
445
930
|
Write-Host " AI Dev Suite (Windows) setup finished" -ForegroundColor Green
|
|
446
931
|
Write-Host "============================================================" -ForegroundColor Green
|
|
447
932
|
Write-Host "Modules : $($selected -join ' ')"
|
|
448
|
-
if ($failed) {
|
|
933
|
+
if ($failed) {
|
|
934
|
+
Write-Host "Failed : $($failed -join ' ')" -ForegroundColor Yellow
|
|
935
|
+
Write-Log ERROR "bootstrap" "completed_with_failures" "Setup finished with failed modules or quality gates" 1 "failed=$($failed -join ',')"
|
|
936
|
+
Write-Host "Report : $ReportFile"
|
|
937
|
+
exit 1
|
|
938
|
+
}
|
|
939
|
+
Write-Log INFO "bootstrap" "completed" "Setup completed successfully" 0
|
|
449
940
|
Write-Host "Report : $ReportFile"
|
|
450
941
|
Write-Host "`nNext: open a new terminal so PATH updates apply."
|