@asterxsk/kiln 0.2.0 → 0.4.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/LICENSE +21 -21
- package/README.md +4 -19
- package/agent/extensions/AGENTS.md +68 -68
- package/agent/extensions/ask-user/install.ps1 +4 -0
- package/agent/extensions/ask-user/install.sh +3 -0
- package/agent/extensions/background-terminals/install.ps1 +4 -0
- package/agent/extensions/background-terminals/install.sh +3 -0
- package/agent/extensions/destructive/install.ps1 +4 -0
- package/agent/extensions/destructive/install.sh +3 -0
- package/agent/extensions/file-search/install.ps1 +4 -0
- package/agent/extensions/file-search/install.sh +3 -0
- package/agent/extensions/goal/install.ps1 +4 -0
- package/agent/extensions/goal/install.sh +3 -0
- package/agent/extensions/herdr-agent-state.ts +257 -0
- package/agent/extensions/kiln-update/README.md +25 -0
- package/agent/extensions/kiln-update/index.ts +99 -0
- package/agent/extensions/kiln-update/install.ps1 +27 -0
- package/agent/extensions/kiln-update/install.sh +24 -0
- package/agent/extensions/modelconf/install.ps1 +4 -0
- package/agent/extensions/modelconf/install.sh +3 -0
- package/agent/extensions/shared/install.ps1 +4 -0
- package/agent/extensions/shared/install.sh +3 -0
- package/agent/extensions/skillsconf/install.ps1 +4 -0
- package/agent/extensions/skillsconf/install.sh +3 -0
- package/agent/extensions/statusline/install.ps1 +4 -0
- package/agent/extensions/statusline/install.sh +3 -0
- package/agent/extensions/subagents/install.ps1 +4 -0
- package/agent/extensions/subagents/install.sh +3 -0
- package/agent/extensions/taste/index.ts +443 -0
- package/agent/extensions/taste/install.ps1 +27 -0
- package/agent/extensions/taste/install.sh +24 -0
- package/agent/extensions/todo/install.ps1 +4 -0
- package/agent/extensions/todo/install.sh +3 -0
- package/agent/extensions/trim-context/install.ps1 +4 -0
- package/agent/extensions/trim-context/install.sh +3 -0
- package/agent/version.txt +1 -0
- package/bin/kiln.js +590 -471
- package/package.json +27 -27
- package/agent/install.ps1 +0 -637
- package/agent/install.sh +0 -620
package/agent/install.ps1
DELETED
|
@@ -1,637 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env pwsh
|
|
2
|
-
<#
|
|
3
|
-
.SYNOPSIS
|
|
4
|
-
Pi custom-setup installer — Windows / PowerShell
|
|
5
|
-
|
|
6
|
-
1) Install/update pi (@earendil-works/pi-coding-agent)
|
|
7
|
-
2) Install pi packages (pi-context-usage, @baretread/pi-forge)
|
|
8
|
-
3) Install custom config (AGENTS.md, keybindings, extensions …)
|
|
9
|
-
4) Install per-extension npm deps via each extension's install.ps1
|
|
10
|
-
|
|
11
|
-
TUI inspired by https://pi.dev/install.ps1 — spinner when TTY, silent otherwise.
|
|
12
|
-
|
|
13
|
-
.EXAMPLE
|
|
14
|
-
powershell -NoProfile -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/asterxsk/kiln/main/agent/install.ps1 -UseBasicParsing | iex"
|
|
15
|
-
.\install.ps1 -RepoUrl https://github.com/asterxsk/kiln -Branch main
|
|
16
|
-
.\install.ps1 -Target "$env:USERPROFILE\.pi\agent" -Yes
|
|
17
|
-
.\install.ps1 -OverwriteSettings # replace settings.json (asks by default)
|
|
18
|
-
|
|
19
|
-
.NOTES
|
|
20
|
-
Safe to re-run. Managed files are force-overwritten; per-user files (settings.json, taste.md, etc.) are preserved
|
|
21
|
-
unless -OverwriteSettings is passed or you answer Y when asked.
|
|
22
|
-
Already-installed items are skipped; outdated global packages are updated.
|
|
23
|
-
Only one timestamped backup (<target>.bak.*) is kept; the clone temp dir
|
|
24
|
-
under ~/.pi/tmp/kiln-* is always removed.
|
|
25
|
-
#>
|
|
26
|
-
|
|
27
|
-
param(
|
|
28
|
-
[string]$RepoUrl = $env:PI_CONFIG_REPO,
|
|
29
|
-
[string]$Branch = $(if ($env:PI_CONFIG_BRANCH) { $env:PI_CONFIG_BRANCH } else { "main" }),
|
|
30
|
-
[string]$Target = "",
|
|
31
|
-
[switch]$SkipPi,
|
|
32
|
-
[switch]$SkipPackages,
|
|
33
|
-
[switch]$Yes,
|
|
34
|
-
[switch]$Local,
|
|
35
|
-
[switch]$OverwriteSettings,
|
|
36
|
-
[switch]$KeepSettings
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
$ErrorActionPreference = "Stop"
|
|
40
|
-
Set-StrictMode -Version Latest
|
|
41
|
-
|
|
42
|
-
$script:ClonedTmp = $null
|
|
43
|
-
$script:StartTime = Get-Date
|
|
44
|
-
|
|
45
|
-
$PiPackage = "@earendil-works/pi-coding-agent"
|
|
46
|
-
$NodeMinimum = [version]"22.19.0"
|
|
47
|
-
$Esc = [char]27
|
|
48
|
-
$Cr = [char]13
|
|
49
|
-
$PiHome = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
|
50
|
-
$PiTmp = Join-Path $PiHome ".pi/tmp"
|
|
51
|
-
try { New-Item -ItemType Directory -Path $PiTmp -Force | Out-Null } catch {}
|
|
52
|
-
$TmpLog = Join-Path $PiTmp ("pi-setup-" + [Guid]::NewGuid().ToString("N").Substring(0,8) + ".log")
|
|
53
|
-
$DefaultRepo = "https://github.com/asterxsk/kiln"
|
|
54
|
-
$env:GIT_TERMINAL_PROMPT = "0"
|
|
55
|
-
$env:GCM_INTERACTIVE = "never"
|
|
56
|
-
$env:GIT_ASKPASS = "echo"
|
|
57
|
-
|
|
58
|
-
# Ensure UTF-8 for glyphs (match pi.dev/install.ps1)
|
|
59
|
-
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8; $OutputEncoding = [Text.Encoding]::UTF8 } catch {}
|
|
60
|
-
try { chcp 65001 2>$null | Out-Null } catch {}
|
|
61
|
-
|
|
62
|
-
# ── VT / ANSI ───────────────────────────────────────────────────────────────
|
|
63
|
-
function Test-IsInteractive {
|
|
64
|
-
return (-not [Console]::IsOutputRedirected) -and ($env:TERM -ne "dumb")
|
|
65
|
-
}
|
|
66
|
-
function Enable-VT {
|
|
67
|
-
if (-not (Test-IsInteractive)) { return $false }
|
|
68
|
-
# kernel32 only exists on Windows
|
|
69
|
-
if (-not $IsWindows -and $env:OS -notmatch "Windows") { return $false }
|
|
70
|
-
if ($null -ne $script:VTEnabled) { return $script:VTEnabled }
|
|
71
|
-
try {
|
|
72
|
-
if (-not ("VT.Native" -as [Type])) {
|
|
73
|
-
Add-Type -Namespace VT -Name Native -MemberDefinition @"
|
|
74
|
-
[DllImport("kernel32.dll", SetLastError=true)] public static extern System.IntPtr GetStdHandle(int n);
|
|
75
|
-
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(System.IntPtr h,out uint m);
|
|
76
|
-
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool SetConsoleMode(System.IntPtr h,uint m);
|
|
77
|
-
"@
|
|
78
|
-
}
|
|
79
|
-
$h = [VT.Native]::GetStdHandle(-11)
|
|
80
|
-
[uint32]$m = 0
|
|
81
|
-
$ok = [VT.Native]::GetConsoleMode($h,[ref]$m)
|
|
82
|
-
if ($ok) { $ok = [VT.Native]::SetConsoleMode($h, ($m -bor 0x0004)) }
|
|
83
|
-
$script:VTEnabled = $ok
|
|
84
|
-
return $ok
|
|
85
|
-
} catch { $script:VTEnabled = $false; return $false }
|
|
86
|
-
}
|
|
87
|
-
function Test-Ansi { return Enable-VT }
|
|
88
|
-
$HasAnsi = Test-Ansi
|
|
89
|
-
|
|
90
|
-
# ── palette (scalar vars, dot-free names; braces delimit) ───────────
|
|
91
|
-
if ($HasAnsi) {
|
|
92
|
-
${creset}="$Esc[0m"; ${cbold}="$Esc[1m"; ${cdim}="$Esc[2m"; ${cgreen}="$Esc[32m"
|
|
93
|
-
${cyellow}="$Esc[33m"; ${cred}="$Esc[31m"; ${ccyan}="$Esc[36m"; ${cgray}="$Esc[90m"
|
|
94
|
-
} else {
|
|
95
|
-
${creset}=""; ${cbold}=""; ${cdim}=""; ${cgreen}=""; ${cyellow}=""; ${cred}=""; ${ccyan}=""; ${cgray}=""
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
# ── title ───────────────────────────────────────────────────────────────────
|
|
99
|
-
function Write-Title {
|
|
100
|
-
if ($HasAnsi) {
|
|
101
|
-
[Console]::Write("${Esc}[1m \u25C6 Pi Setup${Esc}[0m`n${Esc}[2m custom agent config \u00B7 pi + forge + extensions${Esc}[0m`n`n")
|
|
102
|
-
} else {
|
|
103
|
-
Write-Host ""
|
|
104
|
-
Write-Host " Pi Setup"
|
|
105
|
-
Write-Host " custom agent config \xB7 pi + forge + extensions"
|
|
106
|
-
Write-Host ""
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
# ── spinner ─────────────────────────────────────────────────────────────────
|
|
111
|
-
$SpinnerFrames = @('\u280B','\u2819','\u2839','\u2838','\u283C','\u2830','\u2820','\u2826','\u2827','\u2807','\u280F')
|
|
112
|
-
# Fallback to ASCII if font missing — pi.dev uses same
|
|
113
|
-
$SpinnerFrames = @('⠋','⠙','⠹','⠸','⠼','⠰','⠠','⠦','⠧','⠇','⠏')
|
|
114
|
-
$SpinnerIdx = 0
|
|
115
|
-
function Get-Spinner { $s = $SpinnerFrames[$script:SpinnerIdx % $SpinnerFrames.Count]; $script:SpinnerIdx++; return $s }
|
|
116
|
-
|
|
117
|
-
function Write-Line($text) {
|
|
118
|
-
if ($HasAnsi) { [Console]::Write("$text`n") } else { Write-Host $text }
|
|
119
|
-
}
|
|
120
|
-
function Write-StepLine($step, $label, $spin) {
|
|
121
|
-
$prefix = "${cdim}[$step/4]${creset}"
|
|
122
|
-
$frame = if ($spin) { "${cyellow}$spin${creset}" } else { " " }
|
|
123
|
-
$line = " $frame $prefix $label"
|
|
124
|
-
if ($HasAnsi -and (Test-IsInteractive)) {
|
|
125
|
-
# \r + EL (\033[K) so each frame overwrites cleanly regardless of length
|
|
126
|
-
[Console]::Write("$Cr${Esc}[K$line")
|
|
127
|
-
} else {
|
|
128
|
-
Write-Host $line
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function Clear-Line {
|
|
132
|
-
if ($HasAnsi -and (Test-IsInteractive)) { [Console]::Write("$Cr${Esc}[K") }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
# ── run with spinner ────────────────────────────────────────────────────────
|
|
136
|
-
function Invoke-NativeWithSpinner {
|
|
137
|
-
# NOTE: the argument parameter must NOT be named $Args — $Args is an
|
|
138
|
-
# automatic variable, and splatting @Args silently binds the (empty)
|
|
139
|
-
# automatic instead of the parameter, dropping every argument.
|
|
140
|
-
param([string]$Step, [string]$Label, [string]$FilePath, [string[]]$ArgumentList)
|
|
141
|
-
# non-interactive: run synchronously, capture to log
|
|
142
|
-
if (-not (Test-IsInteractive) -or -not $HasAnsi) {
|
|
143
|
-
$out = & $FilePath @ArgumentList 2>&1
|
|
144
|
-
$code = $LASTEXITCODE
|
|
145
|
-
if ($out) { $out | ForEach-Object { Add-Content $TmpLog "$_" } }
|
|
146
|
-
return $code
|
|
147
|
-
}
|
|
148
|
-
$script:SpinnerIdx = 0
|
|
149
|
-
$psi = New-Object Diagnostics.ProcessStartInfo
|
|
150
|
-
$psi.FileName = $FilePath
|
|
151
|
-
# Quote args with spaces
|
|
152
|
-
$psi.Arguments = ($ArgumentList | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }) -join " "
|
|
153
|
-
$psi.RedirectStandardOutput = $true
|
|
154
|
-
$psi.RedirectStandardError = $true
|
|
155
|
-
$psi.UseShellExecute = $false
|
|
156
|
-
$psi.CreateNoWindow = $true
|
|
157
|
-
$p = New-Object Diagnostics.Process
|
|
158
|
-
$p.StartInfo = $psi
|
|
159
|
-
[void]$p.Start()
|
|
160
|
-
while (-not $p.HasExited) {
|
|
161
|
-
Write-StepLine $Step $Label (Get-Spinner)
|
|
162
|
-
Start-Sleep -Milliseconds 80
|
|
163
|
-
}
|
|
164
|
-
$stdout = $p.StandardOutput.ReadToEnd()
|
|
165
|
-
$stderr = $p.StandardError.ReadToEnd()
|
|
166
|
-
if ($stdout) { Add-Content $TmpLog $stdout }
|
|
167
|
-
if ($stderr) { Add-Content $TmpLog $stderr }
|
|
168
|
-
Clear-Line
|
|
169
|
-
return $p.ExitCode
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# ── helpers ─────────────────────────────────────────────────────────────────
|
|
173
|
-
function Resolve-Target {
|
|
174
|
-
if ($Target) { return $Target }
|
|
175
|
-
if ($env:PI_AGENT_DIR) { return $env:PI_AGENT_DIR }
|
|
176
|
-
if ($IsWindows -or $env:OS -match "Windows") {
|
|
177
|
-
return (Join-Path $env:USERPROFILE ".pi\agent")
|
|
178
|
-
}
|
|
179
|
-
$homeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
|
180
|
-
return (Join-Path $homeDir ".pi/agent")
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function Resolve-RepoUrl {
|
|
184
|
-
if ($RepoUrl) { return $RepoUrl }
|
|
185
|
-
# When piped via irm|iex, $PSScriptRoot is empty — don't probe, use default
|
|
186
|
-
$isPiped = -not $PSScriptRoot
|
|
187
|
-
if (-not $isPiped) {
|
|
188
|
-
try {
|
|
189
|
-
$here = $PSScriptRoot
|
|
190
|
-
if (Test-Path (Join-Path $here ".git")) {
|
|
191
|
-
$u = (git -C $here remote get-url origin 2>$null)
|
|
192
|
-
if ($u) { return $u.Trim() }
|
|
193
|
-
}
|
|
194
|
-
$parent = Split-Path $here -Parent
|
|
195
|
-
if (Test-Path (Join-Path $parent ".git")) {
|
|
196
|
-
$u = (git -C $parent remote get-url origin 2>$null)
|
|
197
|
-
if ($u) { return $u.Trim() }
|
|
198
|
-
}
|
|
199
|
-
if (Test-Path (Join-Path (Get-Location).Path ".git")) {
|
|
200
|
-
$u = (git remote get-url origin 2>$null)
|
|
201
|
-
if ($u) { return $u.Trim() }
|
|
202
|
-
}
|
|
203
|
-
} catch {}
|
|
204
|
-
}
|
|
205
|
-
return $DefaultRepo
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function Resolve-SourceRoot {
|
|
209
|
-
param([string]$Repo)
|
|
210
|
-
# Local checkout detection (only when not piped or -Local forced)
|
|
211
|
-
$checkLocal = $Local -or $PSScriptRoot
|
|
212
|
-
if ($checkLocal) {
|
|
213
|
-
$roots = @()
|
|
214
|
-
if ($PSScriptRoot) { $roots += $PSScriptRoot; $roots += (Split-Path $PSScriptRoot -Parent); $roots += (Get-Location).Path }
|
|
215
|
-
else { $roots += (Get-Location).Path }
|
|
216
|
-
foreach ($r in $roots) {
|
|
217
|
-
if ((Test-Path (Join-Path $r "extensions")) -and (Test-Path (Join-Path $r "AGENTS.md"))) { return $r }
|
|
218
|
-
if (Test-Path (Join-Path $r "agent/extensions")) { return (Join-Path $r "agent") }
|
|
219
|
-
}
|
|
220
|
-
if ($Local) { throw "Local checkout not found (tried: $($roots -join ', '))" }
|
|
221
|
-
}
|
|
222
|
-
# Clone to ~/.pi/tmp — simple git clone, no account required, then copy agent folder.
|
|
223
|
-
# Use a unique subdir per run so concurrent installs never clobber each other.
|
|
224
|
-
if (-not (Test-Path $PiTmp)) { New-Item -ItemType Directory -Path $PiTmp -Force | Out-Null }
|
|
225
|
-
$tmp = Join-Path $PiTmp ("kiln-" + [Guid]::NewGuid().ToString("N").Substring(0,8))
|
|
226
|
-
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
|
227
|
-
Write-Line "${cdim} → cloning $Repo (branch $Branch) → $tmp${creset}"
|
|
228
|
-
$env:GIT_TERMINAL_PROMPT = "0"; $env:GIT_ASKPASS = "echo"; $env:GCM_INTERACTIVE = "never"
|
|
229
|
-
$code = Invoke-NativeWithSpinner -Step "3" -Label "fetching config" -FilePath "git" -ArgumentList @("clone","--depth","1","--branch",$Branch,$Repo,$tmp)
|
|
230
|
-
if ($code -ne 0) { throw "git clone failed (exit $code). See $TmpLog" }
|
|
231
|
-
$script:ClonedTmp = $tmp
|
|
232
|
-
if (Test-Path (Join-Path $tmp "agent")) { return (Join-Path $tmp "agent") }
|
|
233
|
-
return $tmp
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function Resolve-Npm {
|
|
237
|
-
# "npm" resolves to npm.ps1 (a PowerShell shim) under Windows PowerShell.
|
|
238
|
-
# npm.ps1 re-parses its own invocation line, which mangles arguments
|
|
239
|
-
# passed via variables/splatting — always prefer the native npm.cmd.
|
|
240
|
-
$cmd = Get-Command "npm.cmd" -ErrorAction SilentlyContinue
|
|
241
|
-
if ($cmd) { return $cmd.Source }
|
|
242
|
-
return "npm"
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function Test-Node {
|
|
246
|
-
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
247
|
-
if (-not $node) { return $null }
|
|
248
|
-
try { $v = (node --version 2>$null).Trim().TrimStart('v').Split('-')[0]; return [version]$v } catch { return $null }
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
# Installed global version of a package ("" when not installed).
|
|
252
|
-
function Get-GlobalPkgVersion {
|
|
253
|
-
param([string]$Name)
|
|
254
|
-
try {
|
|
255
|
-
$bin = Resolve-Npm
|
|
256
|
-
$out = & $bin ls -g $Name --depth=0 2>$null | Out-String
|
|
257
|
-
$m = [regex]::Match($out, [regex]::Escape($Name) + '@(\S+)')
|
|
258
|
-
if ($m.Success) { return ($m.Groups[1].Value -replace '[^0-9A-Za-z.+-].*$','') }
|
|
259
|
-
} catch {}
|
|
260
|
-
return ""
|
|
261
|
-
}
|
|
262
|
-
# Latest registry version ("" when offline / unknown).
|
|
263
|
-
function Get-LatestPkgVersion {
|
|
264
|
-
param([string]$Name)
|
|
265
|
-
try {
|
|
266
|
-
$bin = Resolve-Npm
|
|
267
|
-
$v = ((& $bin view $Name version 2>$null | Out-String).Trim() -split '\s+')[0]
|
|
268
|
-
if ($v -match '^[0-9][0-9A-Za-z.+-]*$') { return $v }
|
|
269
|
-
} catch {}
|
|
270
|
-
return ""
|
|
271
|
-
}
|
|
272
|
-
# Returns $true when the user wants settings.json overwritten.
|
|
273
|
-
function Test-WantOverwriteSettings {
|
|
274
|
-
if ($OverwriteSettings) { return $true }
|
|
275
|
-
if ($KeepSettings) { return $false }
|
|
276
|
-
if ($Yes) { return $false }
|
|
277
|
-
try {
|
|
278
|
-
$ans = Read-Host " settings.json exists. Overwrite with repo defaults? [y/N] (Recommended for first-time setup)"
|
|
279
|
-
return ($ans -eq 'y' -or $ans -eq 'Y' -or $ans -eq 'yes' -or $ans -eq 'YES')
|
|
280
|
-
} catch { return $false }
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
# ── MAIN ─────────────────────────────────────────────────────────────────────
|
|
284
|
-
"" | Set-Content $TmpLog
|
|
285
|
-
# Cleanup trap
|
|
286
|
-
$null = Register-EngineEvent PowerShell.Exiting -Action { if ($script:ClonedTmp -and (Test-Path $script:ClonedTmp)) { Remove-Item $script:ClonedTmp -Recurse -Force -ErrorAction SilentlyContinue } } -ErrorAction SilentlyContinue
|
|
287
|
-
|
|
288
|
-
Write-Title
|
|
289
|
-
$targetDir = Resolve-Target
|
|
290
|
-
$repoUrl = Resolve-RepoUrl
|
|
291
|
-
$ok = $true
|
|
292
|
-
|
|
293
|
-
Write-Line "${cdim} target ${creset}$targetDir"
|
|
294
|
-
Write-Line "${cdim} repo ${creset}$repoUrl ${cdim}($Branch)${creset}"
|
|
295
|
-
Write-Line "${cdim} log ${creset}$TmpLog`n"
|
|
296
|
-
|
|
297
|
-
# ── preflight ────────────────────────────────────────────────────────────────
|
|
298
|
-
$nodeVer = Test-Node
|
|
299
|
-
if (-not $nodeVer) {
|
|
300
|
-
Write-Line "${cred} ✖ Node.js not found. Install Node 22.19+ from https://nodejs.org${creset}"
|
|
301
|
-
Write-Line " then re-run this installer.`n"
|
|
302
|
-
exit 1
|
|
303
|
-
}
|
|
304
|
-
if ($nodeVer -lt $NodeMinimum) {
|
|
305
|
-
Write-Line "${cyellow} ⚠ Node $nodeVer < $NodeMinimum — please upgrade to Node 22.19+${creset}"
|
|
306
|
-
if (-not $Yes) { Write-Line " continue anyway in 3s…"; Start-Sleep 3 }
|
|
307
|
-
}
|
|
308
|
-
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
|
309
|
-
Write-Line "${cred} ✖ npm not found (Node installed but npm missing)${creset}"; exit 1
|
|
310
|
-
}
|
|
311
|
-
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
312
|
-
Write-Line "${cred} ✖ git not found — required to fetch config${creset}"
|
|
313
|
-
Write-Line " install from https://git-scm.com/downloads`n"; exit 1
|
|
314
|
-
}
|
|
315
|
-
$npmVer = (npm --version 2>$null).Trim()
|
|
316
|
-
$gitVer = (git --version 2>$null).Trim()
|
|
317
|
-
Write-Line "${cdim} node $nodeVer · npm $npmVer · $gitVer${creset}`n"
|
|
318
|
-
$NpmBin = Resolve-Npm
|
|
319
|
-
|
|
320
|
-
# ── [1/4] pi ────────────────────────────────────────────────────────────────
|
|
321
|
-
if ($SkipPi) {
|
|
322
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[1/4]${creset} Install pi ${cdim}— skipped${creset}"
|
|
323
|
-
} elseif (Get-Command pi -ErrorAction SilentlyContinue) {
|
|
324
|
-
$piInstalled = Get-GlobalPkgVersion $PiPackage
|
|
325
|
-
if (-not $piInstalled) { $piInstalled = try { (pi --version 2>$null).Trim().TrimStart('v') } catch { "" } }
|
|
326
|
-
$piLatest = Get-LatestPkgVersion $PiPackage
|
|
327
|
-
if ($piInstalled -and $piLatest -and ($piInstalled -eq $piLatest)) {
|
|
328
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[1/4]${creset} pi ${cdim}— already installed ($piInstalled), skipping${creset}"
|
|
329
|
-
} elseif ($piInstalled -and -not $piLatest) {
|
|
330
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[1/4]${creset} pi ${cdim}— already installed ($piInstalled), skipping version check (offline)${creset}"
|
|
331
|
-
} else {
|
|
332
|
-
if ($piInstalled -and $piLatest) { $label = "updating pi $piInstalled → $piLatest ($PiPackage)" } else { $label = "installing pi ($PiPackage)" }
|
|
333
|
-
$code = Invoke-NativeWithSpinner -Step "1" -Label $label -FilePath $NpmBin -ArgumentList @("install","-g","$PiPackage@latest","--no-audit","--no-fund","--min-release-age=0")
|
|
334
|
-
if ($code -eq 0) {
|
|
335
|
-
$piVer = try { (pi --version 2>$null).Trim() } catch { "?" }
|
|
336
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[1/4]${creset} $label ${cdim}— $piVer${creset}"
|
|
337
|
-
} else {
|
|
338
|
-
$isPerm = Select-String -Path $TmpLog -Pattern "EACCES|permission" -Quiet -ErrorAction SilentlyContinue
|
|
339
|
-
if ($isPerm) {
|
|
340
|
-
Write-Line " ${cred}✖${creset} ${cdim}[1/4]${creset} $label ${cred}permission denied${creset}"
|
|
341
|
-
Write-Line " ${cdim}try: npm config set prefix ~/.npm-global or run as admin${creset}"
|
|
342
|
-
} else {
|
|
343
|
-
Write-Line " ${cred}✖${creset} ${cdim}[1/4]${creset} $label ${cred}failed (exit $code)${creset} ${cdim}see $TmpLog${creset}"
|
|
344
|
-
}
|
|
345
|
-
$ok = $false
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
} else {
|
|
349
|
-
$hasPi = $null -ne (Get-Command pi -ErrorAction SilentlyContinue)
|
|
350
|
-
$label = if ($hasPi) { "updating pi ($PiPackage)" } else { "installing pi ($PiPackage)" }
|
|
351
|
-
$code = Invoke-NativeWithSpinner -Step "1" -Label $label -FilePath $NpmBin -ArgumentList @("install","-g","$PiPackage@latest","--no-audit","--no-fund","--min-release-age=0")
|
|
352
|
-
if ($code -eq 0) {
|
|
353
|
-
$piVer = try { (pi --version 2>$null).Trim() } catch { "?" }
|
|
354
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[1/4]${creset} $label ${cdim}— $piVer${creset}"
|
|
355
|
-
} else {
|
|
356
|
-
$isPerm = Select-String -Path $TmpLog -Pattern "EACCES|permission" -Quiet -ErrorAction SilentlyContinue
|
|
357
|
-
if ($isPerm) {
|
|
358
|
-
Write-Line " ${cred}✖${creset} ${cdim}[1/4]${creset} $label ${cred}permission denied${creset}"
|
|
359
|
-
Write-Line " ${cdim}try: npm config set prefix ~/.npm-global or run as admin${creset}"
|
|
360
|
-
} else {
|
|
361
|
-
Write-Line " ${cred}✖${creset} ${cdim}[1/4]${creset} $label ${cred}failed (exit $code)${creset} ${cdim}see $TmpLog${creset}"
|
|
362
|
-
}
|
|
363
|
-
$ok = $false
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
# ── [2/4] pi packages ─────────────────────────────────────────────────────
|
|
368
|
-
if ($SkipPackages) {
|
|
369
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[2/4]${creset} pi packages ${cdim}— skipped${creset}"
|
|
370
|
-
} else {
|
|
371
|
-
$need = @()
|
|
372
|
-
foreach ($pkgName in @("pi-context-usage","@baretread/pi-forge")) {
|
|
373
|
-
$inst = Get-GlobalPkgVersion $pkgName
|
|
374
|
-
$latest = Get-LatestPkgVersion $pkgName
|
|
375
|
-
if ($inst -and $latest -and ($inst -eq $latest)) {
|
|
376
|
-
Write-Line "${cdim} · $pkgName@$inst already installed, skipping${creset}"
|
|
377
|
-
} elseif ($inst -and -not $latest) {
|
|
378
|
-
Write-Line "${cdim} · $pkgName@$inst already installed, skipping version check (offline)${creset}"
|
|
379
|
-
} else {
|
|
380
|
-
if ($inst) { Write-Line "${cdim} · $pkgName $inst → $(if ($latest) { $latest } else { 'latest' }) — will update${creset}" }
|
|
381
|
-
$need += "$pkgName@latest"
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if ($need.Count -eq 0) {
|
|
385
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[2/4]${creset} pi packages ${cdim}— already installed, skipping${creset}"
|
|
386
|
-
} else {
|
|
387
|
-
$label = "installing $($need -join ' ')"
|
|
388
|
-
$code = Invoke-NativeWithSpinner -Step "2" -Label $label -FilePath $NpmBin -ArgumentList (@("install","-g") + $need + @("--no-audit","--no-fund"))
|
|
389
|
-
if ($code -eq 0) {
|
|
390
|
-
# Verify global ls (handles mise/nvm prefix quirks)
|
|
391
|
-
$hasCtx = $null -ne (npm ls -g pi-context-usage 2>$null | Select-String "pi-context-usage")
|
|
392
|
-
$hasForge = $null -ne (npm ls -g "@baretread/pi-forge" 2>$null | Select-String "pi-forge")
|
|
393
|
-
if (-not $hasCtx -or -not $hasForge) {
|
|
394
|
-
Write-Line " ${cyellow}⚠${creset} ${cdim}[2/4]${creset} pi packages installed but not in global ls — continuing${creset}"
|
|
395
|
-
}
|
|
396
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[2/4]${creset} pi packages ${cdim}— done${creset}"
|
|
397
|
-
} else {
|
|
398
|
-
Write-Line " ${cyellow}⚠${creset} ${cdim}[2/4]${creset} $label ${cyellow}exit $code — continuing (pi will auto-install)${creset}"
|
|
399
|
-
Write-Line " ${cdim}see $TmpLog${creset}"
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
# ── [3/4] custom config ───────────────────────────────────────────────────
|
|
405
|
-
try {
|
|
406
|
-
$sourceRoot = Resolve-SourceRoot -Repo $repoUrl
|
|
407
|
-
Write-Line "${cdim} source $sourceRoot${creset}"
|
|
408
|
-
if (-not (Test-Path $sourceRoot)) { throw "source not found: $sourceRoot" }
|
|
409
|
-
|
|
410
|
-
if (-not (Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }
|
|
411
|
-
|
|
412
|
-
# backup (managed files only: extensions + top-level config files).
|
|
413
|
-
# Per-user data (settings.json, auth.json, sessions/, taste/, etc.) is never
|
|
414
|
-
# touched by the installer, so it is deliberately NOT backed up.
|
|
415
|
-
$bak = $null
|
|
416
|
-
$hasExisting = (Test-Path (Join-Path $targetDir "AGENTS.md")) -or (Test-Path (Join-Path $targetDir "extensions"))
|
|
417
|
-
if ($hasExisting) {
|
|
418
|
-
$bak = "$targetDir.bak.$(Get-Date -Format yyyyMMdd-HHmmss)"
|
|
419
|
-
Write-Line "${cdim} backup $targetDir/extensions → $bak${creset}"
|
|
420
|
-
New-Item -ItemType Directory -Path $bak -Force | Out-Null
|
|
421
|
-
foreach ($f in @("AGENTS.md","keybindings.json","README.md")) {
|
|
422
|
-
$s = Join-Path $targetDir $f
|
|
423
|
-
if (Test-Path $s) { Copy-Item $s (Join-Path $bak $f) -Force -ErrorAction SilentlyContinue }
|
|
424
|
-
}
|
|
425
|
-
$srcBakExt = Join-Path $targetDir "extensions"
|
|
426
|
-
if (Test-Path $srcBakExt) {
|
|
427
|
-
# NOTE: Copy-Item -Exclude only filters top-level items, not recursive
|
|
428
|
-
# contents — copy per-extension while skipping node_modules/.git instead.
|
|
429
|
-
$dstBakExt = Join-Path $bak "extensions"
|
|
430
|
-
Get-ChildItem $srcBakExt -Directory | ForEach-Object {
|
|
431
|
-
$d = Join-Path $dstBakExt $_.Name
|
|
432
|
-
New-Item -ItemType Directory -Path $d -Force | Out-Null
|
|
433
|
-
Get-ChildItem $_.FullName -Exclude @("node_modules",".git","bin") | ForEach-Object {
|
|
434
|
-
Copy-Item $_.FullName $d -Recurse -Force -ErrorAction SilentlyContinue
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
# only one backup at a time — delete older siblings, keep the newest
|
|
441
|
-
if ($bak) {
|
|
442
|
-
$bakKeep = Split-Path $bak -Leaf
|
|
443
|
-
$bakBase = Split-Path $targetDir -Leaf
|
|
444
|
-
$bakParent = Split-Path $targetDir -Parent
|
|
445
|
-
Get-ChildItem $bakParent -Directory -Filter "$bakBase.bak.*" -ErrorAction SilentlyContinue |
|
|
446
|
-
Where-Object { $_.Name -ne $bakKeep } |
|
|
447
|
-
ForEach-Object { Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
$copied = 0
|
|
451
|
-
$isSelfInstall = ($sourceRoot -eq $targetDir)
|
|
452
|
-
if ($isSelfInstall) { Write-Line "${cdim} source == target — skipping file copy (self-install)${creset}" }
|
|
453
|
-
foreach ($f in @("AGENTS.md","keybindings.json","README.md")) {
|
|
454
|
-
$s = Join-Path $sourceRoot $f
|
|
455
|
-
if (-not (Test-Path $s)) { continue }
|
|
456
|
-
$d = Join-Path $targetDir $f
|
|
457
|
-
if ($isSelfInstall -or $s -eq $d) { $copied++; continue }
|
|
458
|
-
Copy-Item $s $d -Force; $copied++
|
|
459
|
-
}
|
|
460
|
-
# settings.json: seed from repo defaults if missing; otherwise keep,
|
|
461
|
-
# unless -OverwriteSettings is passed or the user answers Y.
|
|
462
|
-
$repoSettings = Join-Path $sourceRoot "settings.json"
|
|
463
|
-
$settings = Join-Path $targetDir "settings.json"
|
|
464
|
-
$settingsJustCreated = $false
|
|
465
|
-
if ((-not (Test-Path $settings)) -and (Test-Path $repoSettings)) {
|
|
466
|
-
Copy-Item $repoSettings $settings
|
|
467
|
-
$settingsJustCreated = $true
|
|
468
|
-
Write-Line "${cdim} created settings.json from repo defaults${creset}"
|
|
469
|
-
} elseif (Test-Path $settings) {
|
|
470
|
-
if ((Test-WantOverwriteSettings) -and (Test-Path $repoSettings)) {
|
|
471
|
-
if ($bak) { Copy-Item $settings (Join-Path $bak "settings.json") -Force -ErrorAction SilentlyContinue }
|
|
472
|
-
Copy-Item $repoSettings $settings -Force
|
|
473
|
-
$settingsJustCreated = $true
|
|
474
|
-
Write-Line "${cdim} overwrote settings.json from repo defaults${creset}"
|
|
475
|
-
} else {
|
|
476
|
-
Write-Line "${cdim} kept existing settings.json${creset}"
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
# ensure Pi extensions (forge + context) are registered — only on a freshly
|
|
480
|
-
# seeded settings.json; an existing file is left completely untouched.
|
|
481
|
-
if ($settingsJustCreated -and (Test-Path $settings) -and (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
482
|
-
try {
|
|
483
|
-
$patch = "const fs=require('fs');const p=process.argv[1];try{let j=JSON.parse(fs.readFileSync(p,'utf8'));let need=['npm:pi-context-usage','npm:@baretread/pi-forge'];j.packages=j.packages||[];let c=false;for(let pkg of need){if(!j.packages.includes(pkg)){j.packages.push(pkg);c=true}}if(c){fs.writeFileSync(p,JSON.stringify(j,null,2)+'\\n');console.log(' patched settings.json packages -> forge + context') } }catch(e){}"
|
|
484
|
-
node -e $patch $settings 2>$null | ForEach-Object { Write-Line "${cdim} $_${creset}" }
|
|
485
|
-
} catch {}
|
|
486
|
-
}
|
|
487
|
-
# taste.md / taste/ — user-specific, preserve if exists
|
|
488
|
-
foreach ($preserve in @("taste.md","taste","taste.json")) {
|
|
489
|
-
$p = Join-Path $targetDir $preserve
|
|
490
|
-
$src = Join-Path $sourceRoot $preserve
|
|
491
|
-
if (Test-Path $p) {
|
|
492
|
-
Write-Line "${cdim} kept existing $preserve${creset}"
|
|
493
|
-
} elseif (Test-Path $src) {
|
|
494
|
-
Copy-Item $src $p -Recurse -Force -Exclude @("node_modules",".git") -ErrorAction SilentlyContinue
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
# extensions — skip stray .md files, handle spaces
|
|
499
|
-
$srcExt = Join-Path $sourceRoot "extensions"
|
|
500
|
-
$dstExt = Join-Path $targetDir "extensions"
|
|
501
|
-
if (Test-Path $srcExt) {
|
|
502
|
-
if ($isSelfInstall -or $srcExt -eq $dstExt) {
|
|
503
|
-
# Self-install: extensions already in place — just count, don't delete/re-copy
|
|
504
|
-
Get-ChildItem $srcExt -Directory | Where-Object { $_.Name -notlike "*.md" } | ForEach-Object { $copied++ }
|
|
505
|
-
} else {
|
|
506
|
-
if (-not (Test-Path $dstExt)) { New-Item -ItemType Directory -Path $dstExt -Force | Out-Null }
|
|
507
|
-
Get-ChildItem $srcExt -Directory | ForEach-Object {
|
|
508
|
-
if ($_.Name -like "*.md") { return }
|
|
509
|
-
$dst = Join-Path $dstExt $_.Name
|
|
510
|
-
if (Test-Path $dst) { Remove-Item $dst -Recurse -Force -ErrorAction SilentlyContinue }
|
|
511
|
-
# NOTE: Copy-Item -Exclude only filters top-level items, not recursive
|
|
512
|
-
# contents — copy per-item while skipping node_modules/.git instead.
|
|
513
|
-
# (node_modules are rebuilt by each extension's installer in step 4;
|
|
514
|
-
# copying them follows junctions and stalls the install.)
|
|
515
|
-
New-Item -ItemType Directory -Path $dst -Force | Out-Null
|
|
516
|
-
Get-ChildItem $_.FullName -Exclude @("node_modules",".git") | ForEach-Object {
|
|
517
|
-
Copy-Item $_.FullName $dst -Recurse -Force -ErrorAction SilentlyContinue
|
|
518
|
-
}
|
|
519
|
-
$copied++
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
# themes — exclude recursive .pi and nul device file
|
|
525
|
-
$srcThemes = Join-Path $sourceRoot "themes"
|
|
526
|
-
if ((Test-Path $srcThemes) -and -not $isSelfInstall -and $srcThemes -ne (Join-Path $targetDir "themes")) {
|
|
527
|
-
$dstThemes = Join-Path $targetDir "themes"
|
|
528
|
-
if (Test-Path $dstThemes) { Remove-Item $dstThemes -Recurse -Force -ErrorAction SilentlyContinue }
|
|
529
|
-
New-Item -ItemType Directory -Path $dstThemes -Force | Out-Null
|
|
530
|
-
Get-ChildItem $srcThemes -Force | Where-Object { $_.Name -notin @(".pi","nul") } | ForEach-Object {
|
|
531
|
-
Copy-Item $_.FullName (Join-Path $dstThemes $_.Name) -Recurse -Force
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[3/4]${creset} custom config ${cdim}— $copied items → $targetDir${creset}"
|
|
536
|
-
# cleanup cloned temp — per spec: clone into ~/.pi/tmp, copy agent, delete temp
|
|
537
|
-
if ($script:ClonedTmp -and (Test-Path $script:ClonedTmp)) {
|
|
538
|
-
if ($script:ClonedTmp.StartsWith($PiTmp, [StringComparison]::OrdinalIgnoreCase)) {
|
|
539
|
-
Remove-Item $script:ClonedTmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
540
|
-
$script:ClonedTmp = $null
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
# delete any leftover kiln-* temp dirs from interrupted runs
|
|
544
|
-
Get-ChildItem $PiTmp -Directory -Filter "kiln-*" -ErrorAction SilentlyContinue |
|
|
545
|
-
Where-Object { $_.FullName -ne $script:ClonedTmp } |
|
|
546
|
-
ForEach-Object { Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue }
|
|
547
|
-
# repo README → alongside the agent dir (e.g. ~/.pi/README.md)
|
|
548
|
-
$repoRoot = Split-Path $sourceRoot -Parent
|
|
549
|
-
$targetParent = Split-Path $targetDir -Parent
|
|
550
|
-
$repoReadme = Join-Path $repoRoot "README.md"
|
|
551
|
-
if ((Test-Path $repoReadme) -and ($repoRoot -ne $targetParent)) {
|
|
552
|
-
Copy-Item $repoReadme (Join-Path $targetParent "README.md") -Force -ErrorAction SilentlyContinue
|
|
553
|
-
}
|
|
554
|
-
} catch {
|
|
555
|
-
Write-Line " ${cred}✖${creset} ${cdim}[3/4]${creset} custom config ${cred}failed:${creset} $($_.Exception.Message)"
|
|
556
|
-
Write-Line " ${cdim}$($_.ScriptStackTrace)${creset}"
|
|
557
|
-
Write-Line " ${cdim}see $TmpLog${creset}"
|
|
558
|
-
$ok = $false
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
# ── [4/4] per-extension deps ──────────────────────────────────────────────
|
|
562
|
-
try {
|
|
563
|
-
$extRoot = Join-Path $targetDir "extensions"
|
|
564
|
-
$exts = @()
|
|
565
|
-
if (Test-Path $extRoot) { $exts = Get-ChildItem $extRoot -Directory | Where-Object { $_.Name -notlike "*.md" } | Sort-Object Name }
|
|
566
|
-
|
|
567
|
-
if ($exts.Count -eq 0) {
|
|
568
|
-
Write-Line " ${cyellow}⚠${creset} ${cdim}[4/4]${creset} no extensions found at $extRoot"
|
|
569
|
-
} else {
|
|
570
|
-
$failed = @()
|
|
571
|
-
$installed = 0
|
|
572
|
-
$skipped = 0
|
|
573
|
-
foreach ($ext in $exts) {
|
|
574
|
-
$label = "extensions/$($ext.Name)"
|
|
575
|
-
if (Test-Path (Join-Path $ext.FullName "node_modules")) {
|
|
576
|
-
Write-Line " ${cdim} · $label — already installed, skipping${creset}"
|
|
577
|
-
$skipped++
|
|
578
|
-
continue
|
|
579
|
-
}
|
|
580
|
-
$ps1 = Join-Path $ext.FullName "install.ps1"
|
|
581
|
-
$sh = Join-Path $ext.FullName "install.sh"
|
|
582
|
-
$hasPkg = Test-Path (Join-Path $ext.FullName "package.json")
|
|
583
|
-
|
|
584
|
-
if (-not $hasPkg -and -not (Test-Path $ps1)) {
|
|
585
|
-
Write-Line " ${cdim} · $label — no package.json, skipping${creset}"
|
|
586
|
-
continue
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (Test-Path $ps1) {
|
|
590
|
-
# prefer pwsh, fallback to powershell
|
|
591
|
-
$pwsh = Get-Command pwsh -ErrorAction SilentlyContinue
|
|
592
|
-
if ($pwsh) { $code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath $pwsh.Source -ArgumentList @("-NoProfile","-File",$ps1) }
|
|
593
|
-
else { $code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath "powershell" -ArgumentList @("-NoProfile","-ExecutionPolicy","Bypass","-File",$ps1) }
|
|
594
|
-
} elseif (Test-Path $sh) {
|
|
595
|
-
$code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath "sh" -ArgumentList @($sh)
|
|
596
|
-
} else {
|
|
597
|
-
$npmArgs = if (Test-Path (Join-Path $ext.FullName "package-lock.json")) { @("--prefix",$ext.FullName,"ci","--no-audit","--no-fund") } else { @("--prefix",$ext.FullName,"install","--no-audit","--no-fund") }
|
|
598
|
-
$code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath $NpmBin -ArgumentList $npmArgs
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if ($code -eq 0) {
|
|
602
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[4/4]${creset} $label"
|
|
603
|
-
$installed++
|
|
604
|
-
} else {
|
|
605
|
-
Write-Line " ${cred}✖${creset} ${cdim}[4/4]${creset} $label ${cred}failed (exit $code)${creset}"
|
|
606
|
-
$failed += $ext.Name
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
if ($failed.Count -eq 0) {
|
|
610
|
-
Write-Line " ${cgreen}✔${creset} ${cdim}[4/4]${creset} extension deps ${cdim}— $installed installed, $skipped skipped${creset}"
|
|
611
|
-
} else {
|
|
612
|
-
Write-Line " ${cyellow}⚠${creset} ${cdim}[4/4]${creset} extension deps ${cyellow}$($failed.Count) failed:${creset} $($failed -join ', ') ${cdim}see $TmpLog${creset}"
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
} catch {
|
|
616
|
-
Write-Line " ${cred}✖${creset} ${cdim}[4/4]${creset} extension deps ${cred}failed:${creset} $($_.Exception.Message)"
|
|
617
|
-
$ok = $false
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
# ── cleanup + summary ─────────────────────────────────────────────────────
|
|
621
|
-
if ($script:ClonedTmp -and (Test-Path $script:ClonedTmp)) {
|
|
622
|
-
Remove-Item $script:ClonedTmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
$elapsed = (Get-Date) - $script:StartTime
|
|
626
|
-
if ($elapsed.TotalSeconds -ge 60) { $elapsedFmt = "{0}m {1}s" -f [int]$elapsed.TotalMinutes, $elapsed.Seconds }
|
|
627
|
-
else { $elapsedFmt = "{0}s" -f [int]$elapsed.TotalSeconds }
|
|
628
|
-
|
|
629
|
-
Write-Line ""
|
|
630
|
-
if ($ok) {
|
|
631
|
-
Write-Line "${cgreen}${cbold} ✓ Setup complete${creset} ${cdim}→ $targetDir · Kiln installed in $elapsedFmt${creset}"
|
|
632
|
-
$piVer2 = try { (pi --version 2>$null).Trim() } catch { $null }
|
|
633
|
-
if ($piVer2) { Write-Line "${cdim} pi $piVer2 · run: pi${creset}" }
|
|
634
|
-
} else {
|
|
635
|
-
Write-Line "${cyellow}${cbold} ⚠ Setup finished with warnings${creset} ${cdim}in $elapsedFmt · see $TmpLog${creset}"
|
|
636
|
-
}
|
|
637
|
-
Write-Line "${cdim} log: $TmpLog${creset}`n"
|