@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/setup-ai.ps1 CHANGED
@@ -1,12 +1,12 @@
1
- #Requires -Version 7.0
1
+ #Requires -Version 7.3
2
2
  <#
3
3
  ==============================================================================
4
4
  AI Dev Suite — Engineering Excellence Edition (Windows)
5
- Version: 3.0.5
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, `go install` for gentle-ai, npm for opencode, and
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.5"
45
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
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 = "micio86dev/Engineering-Excellence"
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([string]$Phase, [scriptblock]$Action, [switch]$Optional)
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 Install-Winget {
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
- Write-Log WARN $Phase "winget_missing" "winget not available; install '$Id' manually"
119
- return
216
+ throw "winget not available; cannot install '$Id'"
120
217
  }
121
- # Skip if already installed.
122
- $installed = winget list --id $Id -e 2>$null | Select-String -SimpleMatch $Id
123
- if ($installed) {
124
- Write-Log INFO $Phase "already_present" "$Id already installed"
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 -Optional -Action {
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 -Optional -Action {
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','engram','codex','antigravity','opencode','cockpit')
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' = 'Core dev tools via winget (git, gh, python, neovim)'
149
- 'node' = 'Node.js LTS (winget OpenJS.NodeJS.LTS) + npm@latest'
150
- 'bun' = 'Bun runtime (bun.sh install.ps1)'
151
- 'pi' = 'pi.dev coding agent CLI (pi.dev install.ps1)'
152
- 'go' = 'Go toolchain (winget GoLang.Go)'
153
- 'ee' = 'Engineering Excellence skill (npx skills add, detected agents)'
154
- 'pi-workflows' = 'pi-extensible-workflows (module resolution fix for pi extensions)'
155
- 'herdr' = 'herdr terminal multiplexer (herdr.dev install.ps1)'
156
- 'gentle-ai' = 'gentle-ai (go install) + gentle-pi package'
157
- 'engram' = 'Engram persistent memory for pi (gentle-engram: /remember /recall)'
158
- 'codex' = 'OpenAI Codex CLI (chatgpt.com install.ps1)'
159
- 'antigravity' = 'Google Antigravity CLI (antigravity.google install.ps1)'
160
- 'opencode' = 'opencode agent CLI (npm opencode-ai)'
161
- 'cockpit' = 'cockpit-tools desktop GUI (optional, .msi, CC BY-NC-SA)'
162
- }
163
- $ModuleOptional = @{ 'cockpit' = $true }
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
- Install-Winget -Id "OpenJS.NodeJS.LTS" -Phase "node"
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
- Invoke-Step -Phase "node" -Optional -Action { npm install -g npm@latest }
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) { Write-Log INFO "bun" "already_present" "bun already installed"; return }
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
- if (Test-Cmd pi) { Write-Log INFO "pi" "already_present" "pi already installed"; return }
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) { Write-Log INFO "go" "already_present" "Go already installed"; return }
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)) { Write-Log WARN "ee" "npx_missing" "npx not found; install node first"; return }
219
- # Keys are `skills` CLI agent names (claude-code, gemini-cli), not dir names.
220
- $agents = @{ pi = ".pi"; 'claude-code' = ".claude"; 'gemini-cli' = ".gemini"; cursor = ".cursor"; antigravity = ".antigravity"; codex = ".codex"; opencode = ".config\opencode" }
221
- $any = $false
222
- foreach ($a in @('pi','claude-code','gemini-cli','cursor','antigravity','codex','opencode')) {
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
- if (-not $any) {
231
- Invoke-Step -Phase "ee" -Optional -Action {
232
- npx --yes skills@latest add $EE_Slug --skill $EE_Skill --global --agent pi --copy --yes
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
- if (Test-Path (Join-Path $PiSkillDir "SKILL.md")) {
236
- Write-Log INFO "ee" "skill_installed" "EE skill present for pi"
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)) { Write-Log WARN "pi-workflows" "pi_missing" "pi not found; skipped"; return }
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 { $ver = (npm view pi-extensible-workflows version).Trim() } catch {}
245
- if (-not $ver) { Write-Log WARN "pi-workflows" "version_unresolved" "Could not resolve version; skipped"; return }
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" -Optional -Action { pi install "npm:pi-extensible-workflows@$ver" }
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" -Optional -Action {
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) { Write-Log INFO "herdr" "already_present" "herdr already installed"; return }
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
- if (-not (Test-Cmd gentle-ai) -and -not (Test-Cmd gga)) {
270
- if (Test-Cmd go) {
271
- Invoke-Step -Phase "gentle-ai" -Optional -Action {
272
- go install github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@latest
273
- }
274
- } else {
275
- Write-Log WARN "gentle-ai" "go_missing" "Go not found; cannot 'go install' gentle-ai on Windows"
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" -Optional -Action { pi install npm:gentle-pi }
280
- Invoke-Step -Phase "gentle-ai" -Optional -Action { pi install npm:pi-mcp-adapter }
281
- Write-Log INFO "gentle-ai" "pi_enabled" "gentle-pi registered in pi (verify: /gentle-ai:status)"
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
- Invoke-Step -Phase "opencode" -Optional -Action { npm install -g opencode-ai }
335
- } else {
336
- Write-Log WARN "opencode" "npm_missing" "npm not found; install node first"
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 { Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /qb" -Wait }
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}; 'engram' = ${function:Mod-Engram}
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 <csv> | -All | (default = core)"
777
+ Write-Host "`nUse: -Only csv | -All | (default = core)"
380
778
  }
381
779
 
382
780
  function Show-Help {
383
- Get-Content $MyInvocation.MyCommand.Path | Select-Object -First 30 | ForEach-Object { $_ }
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 ($Only) {
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 { $failed += $m; Write-Log ERROR "modules" "module_failed" "Module $m failed: $($_.Exception.Message)" 1 }
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: $(if (Test-Cmd node) { node --version } else { 'n/a' })
437
- - npm: $(if (Test-Cmd npm) { npm --version } else { 'n/a' })
438
- - Go: $(if (Test-Cmd go) { (go version) } else { 'n/a' })
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) { Write-Host "Failed : $($failed -join ' ')" -ForegroundColor Yellow }
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."