@asterxsk/kiln 0.1.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.
Files changed (263) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/agent/AGENTS.md +67 -0
  4. package/agent/README.md +5 -0
  5. package/agent/extensions/AGENTS.md +68 -0
  6. package/agent/extensions/ask-user/index.ts +418 -0
  7. package/agent/extensions/ask-user/install.ps1 +23 -0
  8. package/agent/extensions/ask-user/install.sh +21 -0
  9. package/agent/extensions/ask-user/package-lock.json +769 -0
  10. package/agent/extensions/ask-user/package.json +19 -0
  11. package/agent/extensions/ask-user/prompt.ts +45 -0
  12. package/agent/extensions/ask-user/tsconfig.json +7 -0
  13. package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -0
  14. package/agent/extensions/background-terminals/index.ts +627 -0
  15. package/agent/extensions/background-terminals/install.ps1 +23 -0
  16. package/agent/extensions/background-terminals/install.sh +21 -0
  17. package/agent/extensions/background-terminals/manager.test.ts +735 -0
  18. package/agent/extensions/background-terminals/output.test.ts +109 -0
  19. package/agent/extensions/background-terminals/package-lock.json +769 -0
  20. package/agent/extensions/background-terminals/package.json +17 -0
  21. package/agent/extensions/background-terminals/prompt.test.ts +125 -0
  22. package/agent/extensions/background-terminals/ps.test.ts +82 -0
  23. package/agent/extensions/background-terminals/result-delivery.test.ts +44 -0
  24. package/agent/extensions/background-terminals/src/domain.ts +87 -0
  25. package/agent/extensions/background-terminals/src/manager.ts +907 -0
  26. package/agent/extensions/background-terminals/src/output.ts +84 -0
  27. package/agent/extensions/background-terminals/src/prompt.ts +142 -0
  28. package/agent/extensions/background-terminals/src/result-delivery.ts +27 -0
  29. package/agent/extensions/background-terminals/src/runtime.ts +36 -0
  30. package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -0
  31. package/agent/extensions/background-terminals/src/ui/ps.ts +621 -0
  32. package/agent/extensions/background-terminals/tsconfig.json +7 -0
  33. package/agent/extensions/destructive/README.md +31 -0
  34. package/agent/extensions/destructive/index.ts +88 -0
  35. package/agent/extensions/destructive/install.ps1 +23 -0
  36. package/agent/extensions/destructive/install.sh +21 -0
  37. package/agent/extensions/file-search/index.spec.ts +443 -0
  38. package/agent/extensions/file-search/index.ts +459 -0
  39. package/agent/extensions/file-search/install.ps1 +23 -0
  40. package/agent/extensions/file-search/install.sh +21 -0
  41. package/agent/extensions/file-search/package-lock.json +2253 -0
  42. package/agent/extensions/file-search/package.json +23 -0
  43. package/agent/extensions/file-search/src/args.ts +122 -0
  44. package/agent/extensions/file-search/src/binaries.ts +422 -0
  45. package/agent/extensions/file-search/src/output.ts +126 -0
  46. package/agent/extensions/file-search/src/process.ts +146 -0
  47. package/agent/extensions/file-search/src/prompt.ts +52 -0
  48. package/agent/extensions/file-search/tsconfig.json +7 -0
  49. package/agent/extensions/goal/README.md +50 -0
  50. package/agent/extensions/goal/index.ts +155 -0
  51. package/agent/extensions/goal/install.ps1 +23 -0
  52. package/agent/extensions/goal/install.sh +21 -0
  53. package/agent/extensions/modelconf/PLAN.md +915 -0
  54. package/agent/extensions/modelconf/README.md +66 -0
  55. package/agent/extensions/modelconf/index.ts +296 -0
  56. package/agent/extensions/modelconf/install.ps1 +23 -0
  57. package/agent/extensions/modelconf/install.sh +21 -0
  58. package/agent/extensions/modelconf/package-lock.json +1809 -0
  59. package/agent/extensions/modelconf/package.json +13 -0
  60. package/agent/extensions/modelconf/src/fuzzy.ts +26 -0
  61. package/agent/extensions/modelconf/src/glob.ts +13 -0
  62. package/agent/extensions/modelconf/src/persistence.test.ts +46 -0
  63. package/agent/extensions/modelconf/src/persistence.ts +164 -0
  64. package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
  65. package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
  66. package/agent/extensions/modelconf/tsconfig.json +15 -0
  67. package/agent/extensions/pi-web-access/CHANGELOG.md +690 -0
  68. package/agent/extensions/pi-web-access/LICENSE +21 -0
  69. package/agent/extensions/pi-web-access/README.md +470 -0
  70. package/agent/extensions/pi-web-access/SECURITY.md +5 -0
  71. package/agent/extensions/pi-web-access/activity.ts +101 -0
  72. package/agent/extensions/pi-web-access/auth-fetch.ts +148 -0
  73. package/agent/extensions/pi-web-access/banner.png +0 -0
  74. package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
  75. package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -0
  76. package/agent/extensions/pi-web-access/content-find.ts +139 -0
  77. package/agent/extensions/pi-web-access/credential-source.ts +191 -0
  78. package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
  79. package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
  80. package/agent/extensions/pi-web-access/declared-web-links.ts +173 -0
  81. package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
  82. package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
  83. package/agent/extensions/pi-web-access/exa.ts +526 -0
  84. package/agent/extensions/pi-web-access/extract.ts +1196 -0
  85. package/agent/extensions/pi-web-access/feature-config.ts +29 -0
  86. package/agent/extensions/pi-web-access/fetch-params.ts +111 -0
  87. package/agent/extensions/pi-web-access/gemini-adc.ts +298 -0
  88. package/agent/extensions/pi-web-access/gemini-api.ts +353 -0
  89. package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
  90. package/agent/extensions/pi-web-access/gemini-search.ts +21 -0
  91. package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -0
  92. package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -0
  93. package/agent/extensions/pi-web-access/gemini-web.ts +487 -0
  94. package/agent/extensions/pi-web-access/github-api.ts +197 -0
  95. package/agent/extensions/pi-web-access/github-extract.ts +746 -0
  96. package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -0
  97. package/agent/extensions/pi-web-access/index.ts +1737 -0
  98. package/agent/extensions/pi-web-access/package-lock.json +5808 -0
  99. package/agent/extensions/pi-web-access/package.json +64 -0
  100. package/agent/extensions/pi-web-access/page-query.ts +96 -0
  101. package/agent/extensions/pi-web-access/pdf-extract.ts +409 -0
  102. package/agent/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  103. package/agent/extensions/pi-web-access/promise-try.d.ts +7 -0
  104. package/agent/extensions/pi-web-access/query-rewrite.ts +51 -0
  105. package/agent/extensions/pi-web-access/render-search-error.ts +170 -0
  106. package/agent/extensions/pi-web-access/rsc-extract.ts +338 -0
  107. package/agent/extensions/pi-web-access/search-types.ts +20 -0
  108. package/agent/extensions/pi-web-access/source-check.ts +282 -0
  109. package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -0
  110. package/agent/extensions/pi-web-access/storage.ts +521 -0
  111. package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -0
  112. package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
  113. package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
  114. package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
  115. package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -0
  116. package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -0
  117. package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
  118. package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
  119. package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
  120. package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
  121. package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
  122. package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
  123. package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
  124. package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
  125. package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
  126. package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
  127. package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
  128. package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
  129. package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
  130. package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
  131. package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
  132. package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
  133. package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
  134. package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
  135. package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
  136. package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
  137. package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
  138. package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
  139. package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
  140. package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
  141. package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -0
  142. package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
  143. package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
  144. package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
  145. package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
  146. package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
  147. package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
  148. package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
  149. package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -0
  150. package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
  151. package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
  152. package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
  153. package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
  154. package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
  155. package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
  156. package/agent/extensions/pi-web-access/tsconfig.json +11 -0
  157. package/agent/extensions/pi-web-access/utils.ts +451 -0
  158. package/agent/extensions/pi-web-access/video-extract.ts +392 -0
  159. package/agent/extensions/pi-web-access/youtube-extract.ts +328 -0
  160. package/agent/extensions/shared/activity-status.ts +31 -0
  161. package/agent/extensions/shared/child-session.test.ts +270 -0
  162. package/agent/extensions/shared/child-session.ts +148 -0
  163. package/agent/extensions/shared/context-utilization.test.ts +48 -0
  164. package/agent/extensions/shared/context-utilization.ts +47 -0
  165. package/agent/extensions/shared/dashboard-state.ts +99 -0
  166. package/agent/extensions/shared/install.ps1 +23 -0
  167. package/agent/extensions/shared/install.sh +21 -0
  168. package/agent/extensions/shared/tool-call-timeout.test.ts +117 -0
  169. package/agent/extensions/shared/tool-call-timeout.ts +104 -0
  170. package/agent/extensions/skillsconf/README.md +74 -0
  171. package/agent/extensions/skillsconf/index.ts +92 -0
  172. package/agent/extensions/skillsconf/install.ps1 +23 -0
  173. package/agent/extensions/skillsconf/install.sh +21 -0
  174. package/agent/extensions/skillsconf/package-lock.json +1809 -0
  175. package/agent/extensions/skillsconf/package.json +18 -0
  176. package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -0
  177. package/agent/extensions/skillsconf/src/delete-skill.ts +45 -0
  178. package/agent/extensions/skillsconf/src/filter.test.ts +80 -0
  179. package/agent/extensions/skillsconf/src/filter.ts +74 -0
  180. package/agent/extensions/skillsconf/src/fuzzy.ts +26 -0
  181. package/agent/extensions/skillsconf/src/persistence.test.ts +72 -0
  182. package/agent/extensions/skillsconf/src/persistence.ts +169 -0
  183. package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
  184. package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
  185. package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -0
  186. package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
  187. package/agent/extensions/skillsconf/tsconfig.json +15 -0
  188. package/agent/extensions/status line/index.ts +282 -0
  189. package/agent/extensions/status line/install.ps1 +23 -0
  190. package/agent/extensions/status line/install.sh +21 -0
  191. package/agent/extensions/subagents/by-the-way.test.ts +29 -0
  192. package/agent/extensions/subagents/claude.test.ts +119 -0
  193. package/agent/extensions/subagents/codex.test.ts +102 -0
  194. package/agent/extensions/subagents/context-usage.test.ts +107 -0
  195. package/agent/extensions/subagents/docs/design-plan.md +568 -0
  196. package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
  197. package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -0
  198. package/agent/extensions/subagents/index.ts +779 -0
  199. package/agent/extensions/subagents/install.ps1 +23 -0
  200. package/agent/extensions/subagents/install.sh +21 -0
  201. package/agent/extensions/subagents/manager.test.ts +276 -0
  202. package/agent/extensions/subagents/package-lock.json +2244 -0
  203. package/agent/extensions/subagents/package.json +19 -0
  204. package/agent/extensions/subagents/result-delivery.test.ts +27 -0
  205. package/agent/extensions/subagents/src/backend.ts +73 -0
  206. package/agent/extensions/subagents/src/backends/claude.ts +701 -0
  207. package/agent/extensions/subagents/src/backends/codex.ts +1060 -0
  208. package/agent/extensions/subagents/src/backends/pi.ts +575 -0
  209. package/agent/extensions/subagents/src/backends/stub.ts +300 -0
  210. package/agent/extensions/subagents/src/by-the-way.ts +21 -0
  211. package/agent/extensions/subagents/src/domain.ts +253 -0
  212. package/agent/extensions/subagents/src/format.ts +74 -0
  213. package/agent/extensions/subagents/src/manager.ts +736 -0
  214. package/agent/extensions/subagents/src/prompt.ts +92 -0
  215. package/agent/extensions/subagents/src/result-delivery.ts +20 -0
  216. package/agent/extensions/subagents/src/runtime.ts +53 -0
  217. package/agent/extensions/subagents/src/ui/takeover.ts +583 -0
  218. package/agent/extensions/subagents/src/ui/transcript.ts +201 -0
  219. package/agent/extensions/subagents/takeover.test.ts +29 -0
  220. package/agent/extensions/subagents/tsconfig.json +7 -0
  221. package/agent/extensions/taste/index.ts +443 -0
  222. package/agent/extensions/taste/install.ps1 +23 -0
  223. package/agent/extensions/taste/install.sh +21 -0
  224. package/agent/extensions/todo/AGENTS.md +38 -0
  225. package/agent/extensions/todo/LICENSE +21 -0
  226. package/agent/extensions/todo/config.ts +55 -0
  227. package/agent/extensions/todo/index.ts +151 -0
  228. package/agent/extensions/todo/install.ps1 +23 -0
  229. package/agent/extensions/todo/install.sh +21 -0
  230. package/agent/extensions/todo/locales/de.json +17 -0
  231. package/agent/extensions/todo/locales/en.json +15 -0
  232. package/agent/extensions/todo/locales/es.json +17 -0
  233. package/agent/extensions/todo/locales/fr.json +17 -0
  234. package/agent/extensions/todo/locales/pt-BR.json +17 -0
  235. package/agent/extensions/todo/locales/pt.json +17 -0
  236. package/agent/extensions/todo/locales/ru.json +17 -0
  237. package/agent/extensions/todo/locales/uk.json +17 -0
  238. package/agent/extensions/todo/locales/zh.json +17 -0
  239. package/agent/extensions/todo/package-lock.json +3358 -0
  240. package/agent/extensions/todo/package.json +67 -0
  241. package/agent/extensions/todo/state/i18n-bridge.ts +64 -0
  242. package/agent/extensions/todo/state/invariants.ts +20 -0
  243. package/agent/extensions/todo/state/replay.ts +38 -0
  244. package/agent/extensions/todo/state/selectors.ts +107 -0
  245. package/agent/extensions/todo/state/state-reducer.ts +326 -0
  246. package/agent/extensions/todo/state/state.ts +18 -0
  247. package/agent/extensions/todo/state/store.ts +82 -0
  248. package/agent/extensions/todo/state/task-graph.ts +57 -0
  249. package/agent/extensions/todo/todo-overlay.ts +200 -0
  250. package/agent/extensions/todo/todo.ts +155 -0
  251. package/agent/extensions/todo/tool/response-envelope.ts +109 -0
  252. package/agent/extensions/todo/tool/types.ts +206 -0
  253. package/agent/extensions/todo/verify-ref-system.js +0 -0
  254. package/agent/extensions/todo/view/format.ts +177 -0
  255. package/agent/extensions/trim-context/README.md +54 -0
  256. package/agent/extensions/trim-context/index.ts +487 -0
  257. package/agent/extensions/trim-context/install.ps1 +23 -0
  258. package/agent/extensions/trim-context/install.sh +21 -0
  259. package/agent/install.ps1 +527 -0
  260. package/agent/install.sh +511 -0
  261. package/agent/keybindings.json +7 -0
  262. package/bin/kiln.js +359 -0
  263. package/package.json +21 -0
@@ -0,0 +1,527 @@
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
+
18
+ .NOTES
19
+ Safe to re-run. Managed files are force-overwritten; per-user files (settings.json, taste.md, etc.) are preserved.
20
+ #>
21
+
22
+ param(
23
+ [string]$RepoUrl = $env:PI_CONFIG_REPO,
24
+ [string]$Branch = $(if ($env:PI_CONFIG_BRANCH) { $env:PI_CONFIG_BRANCH } else { "main" }),
25
+ [string]$Target = "",
26
+ [switch]$SkipPi,
27
+ [switch]$SkipPackages,
28
+ [switch]$Yes,
29
+ [switch]$Local
30
+ )
31
+
32
+ $ErrorActionPreference = "Stop"
33
+ Set-StrictMode -Version Latest
34
+
35
+ $script:ClonedTmp = $null
36
+ $script:StartTime = Get-Date
37
+
38
+ $PiPackage = "@earendil-works/pi-coding-agent"
39
+ $NodeMinimum = [version]"22.19.0"
40
+ $Esc = [char]27
41
+ $Cr = [char]13
42
+ $PiHome = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
43
+ $PiTmp = Join-Path $PiHome ".pi/tmp"
44
+ try { New-Item -ItemType Directory -Path $PiTmp -Force | Out-Null } catch {}
45
+ $TmpLog = Join-Path $PiTmp ("pi-setup-" + [Guid]::NewGuid().ToString("N").Substring(0,8) + ".log")
46
+ $DefaultRepo = "https://github.com/asterxsk/kiln"
47
+ $env:GIT_TERMINAL_PROMPT = "0"
48
+ $env:GCM_INTERACTIVE = "never"
49
+ $env:GIT_ASKPASS = "echo"
50
+
51
+ # Ensure UTF-8 for glyphs (match pi.dev/install.ps1)
52
+ try { [Console]::OutputEncoding = [Text.Encoding]::UTF8; $OutputEncoding = [Text.Encoding]::UTF8 } catch {}
53
+ try { chcp 65001 2>$null | Out-Null } catch {}
54
+
55
+ # ── VT / ANSI ───────────────────────────────────────────────────────────────
56
+ function Test-IsInteractive {
57
+ return (-not [Console]::IsOutputRedirected) -and ($env:TERM -ne "dumb")
58
+ }
59
+ function Enable-VT {
60
+ if (-not (Test-IsInteractive)) { return $false }
61
+ # kernel32 only exists on Windows
62
+ if (-not $IsWindows -and $env:OS -notmatch "Windows") { return $false }
63
+ if ($null -ne $script:VTEnabled) { return $script:VTEnabled }
64
+ try {
65
+ if (-not ("VT.Native" -as [Type])) {
66
+ Add-Type -Namespace VT -Name Native -MemberDefinition @"
67
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern System.IntPtr GetStdHandle(int n);
68
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(System.IntPtr h,out uint m);
69
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern bool SetConsoleMode(System.IntPtr h,uint m);
70
+ "@
71
+ }
72
+ $h = [VT.Native]::GetStdHandle(-11)
73
+ [uint32]$m = 0
74
+ $ok = [VT.Native]::GetConsoleMode($h,[ref]$m)
75
+ if ($ok) { $ok = [VT.Native]::SetConsoleMode($h, ($m -bor 0x0004)) }
76
+ $script:VTEnabled = $ok
77
+ return $ok
78
+ } catch { $script:VTEnabled = $false; return $false }
79
+ }
80
+ function Test-Ansi { return Enable-VT }
81
+ $HasAnsi = Test-Ansi
82
+
83
+ # ── palette (scalar vars, dot-free names; braces delimit) ───────────
84
+ if ($HasAnsi) {
85
+ ${creset}="$Esc[0m"; ${cbold}="$Esc[1m"; ${cdim}="$Esc[2m"; ${cgreen}="$Esc[32m"
86
+ ${cyellow}="$Esc[33m"; ${cred}="$Esc[31m"; ${ccyan}="$Esc[36m"; ${cgray}="$Esc[90m"
87
+ } else {
88
+ ${creset}=""; ${cbold}=""; ${cdim}=""; ${cgreen}=""; ${cyellow}=""; ${cred}=""; ${ccyan}=""; ${cgray}=""
89
+ }
90
+
91
+ # ── title ───────────────────────────────────────────────────────────────────
92
+ function Write-Title {
93
+ if ($HasAnsi) {
94
+ [Console]::Write("${Esc}[1m \u25C6 Pi Setup${Esc}[0m`n${Esc}[2m custom agent config \u00B7 pi + forge + extensions${Esc}[0m`n`n")
95
+ } else {
96
+ Write-Host ""
97
+ Write-Host " Pi Setup"
98
+ Write-Host " custom agent config \xB7 pi + forge + extensions"
99
+ Write-Host ""
100
+ }
101
+ }
102
+
103
+ # ── spinner ─────────────────────────────────────────────────────────────────
104
+ $SpinnerFrames = @('\u280B','\u2819','\u2839','\u2838','\u283C','\u2830','\u2820','\u2826','\u2827','\u2807','\u280F')
105
+ # Fallback to ASCII if font missing — pi.dev uses same
106
+ $SpinnerFrames = @('⠋','⠙','⠹','⠸','⠼','⠰','⠠','⠦','⠧','⠇','⠏')
107
+ $SpinnerIdx = 0
108
+ function Get-Spinner { $s = $SpinnerFrames[$script:SpinnerIdx % $SpinnerFrames.Count]; $script:SpinnerIdx++; return $s }
109
+
110
+ function Write-Line($text) {
111
+ if ($HasAnsi) { [Console]::Write("$text`n") } else { Write-Host $text }
112
+ }
113
+ function Write-StepLine($step, $label, $spin) {
114
+ $prefix = "${cdim}[$step/4]${creset}"
115
+ $frame = if ($spin) { "${cyellow}$spin${creset}" } else { " " }
116
+ $line = " $frame $prefix $label"
117
+ if ($HasAnsi -and (Test-IsInteractive)) {
118
+ # \r + EL (\033[K) so each frame overwrites cleanly regardless of length
119
+ [Console]::Write("$Cr${Esc}[K$line")
120
+ } else {
121
+ Write-Host $line
122
+ }
123
+ }
124
+ function Clear-Line {
125
+ if ($HasAnsi -and (Test-IsInteractive)) { [Console]::Write("$Cr${Esc}[K") }
126
+ }
127
+
128
+ # ── run with spinner ────────────────────────────────────────────────────────
129
+ function Invoke-NativeWithSpinner {
130
+ # NOTE: the argument parameter must NOT be named $Args — $Args is an
131
+ # automatic variable, and splatting @Args silently binds the (empty)
132
+ # automatic instead of the parameter, dropping every argument.
133
+ param([string]$Step, [string]$Label, [string]$FilePath, [string[]]$ArgumentList)
134
+ # non-interactive: run synchronously, capture to log
135
+ if (-not (Test-IsInteractive) -or -not $HasAnsi) {
136
+ $out = & $FilePath @ArgumentList 2>&1
137
+ $code = $LASTEXITCODE
138
+ if ($out) { $out | ForEach-Object { Add-Content $TmpLog "$_" } }
139
+ return $code
140
+ }
141
+ $script:SpinnerIdx = 0
142
+ $psi = New-Object Diagnostics.ProcessStartInfo
143
+ $psi.FileName = $FilePath
144
+ # Quote args with spaces
145
+ $psi.Arguments = ($ArgumentList | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }) -join " "
146
+ $psi.RedirectStandardOutput = $true
147
+ $psi.RedirectStandardError = $true
148
+ $psi.UseShellExecute = $false
149
+ $psi.CreateNoWindow = $true
150
+ $p = New-Object Diagnostics.Process
151
+ $p.StartInfo = $psi
152
+ [void]$p.Start()
153
+ while (-not $p.HasExited) {
154
+ Write-StepLine $Step $Label (Get-Spinner)
155
+ Start-Sleep -Milliseconds 80
156
+ }
157
+ $stdout = $p.StandardOutput.ReadToEnd()
158
+ $stderr = $p.StandardError.ReadToEnd()
159
+ if ($stdout) { Add-Content $TmpLog $stdout }
160
+ if ($stderr) { Add-Content $TmpLog $stderr }
161
+ Clear-Line
162
+ return $p.ExitCode
163
+ }
164
+
165
+ # ── helpers ─────────────────────────────────────────────────────────────────
166
+ function Resolve-Target {
167
+ if ($Target) { return $Target }
168
+ if ($env:PI_AGENT_DIR) { return $env:PI_AGENT_DIR }
169
+ if ($IsWindows -or $env:OS -match "Windows") {
170
+ return (Join-Path $env:USERPROFILE ".pi\agent")
171
+ }
172
+ $homeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
173
+ return (Join-Path $homeDir ".pi/agent")
174
+ }
175
+
176
+ function Resolve-RepoUrl {
177
+ if ($RepoUrl) { return $RepoUrl }
178
+ # When piped via irm|iex, $PSScriptRoot is empty — don't probe, use default
179
+ $isPiped = -not $PSScriptRoot
180
+ if (-not $isPiped) {
181
+ try {
182
+ $here = $PSScriptRoot
183
+ if (Test-Path (Join-Path $here ".git")) {
184
+ $u = (git -C $here remote get-url origin 2>$null)
185
+ if ($u) { return $u.Trim() }
186
+ }
187
+ $parent = Split-Path $here -Parent
188
+ if (Test-Path (Join-Path $parent ".git")) {
189
+ $u = (git -C $parent remote get-url origin 2>$null)
190
+ if ($u) { return $u.Trim() }
191
+ }
192
+ if (Test-Path (Join-Path (Get-Location).Path ".git")) {
193
+ $u = (git remote get-url origin 2>$null)
194
+ if ($u) { return $u.Trim() }
195
+ }
196
+ } catch {}
197
+ }
198
+ return $DefaultRepo
199
+ }
200
+
201
+ function Resolve-SourceRoot {
202
+ param([string]$Repo)
203
+ # Local checkout detection (only when not piped or -Local forced)
204
+ $checkLocal = $Local -or $PSScriptRoot
205
+ if ($checkLocal) {
206
+ $roots = @()
207
+ if ($PSScriptRoot) { $roots += $PSScriptRoot; $roots += (Split-Path $PSScriptRoot -Parent); $roots += (Get-Location).Path }
208
+ else { $roots += (Get-Location).Path }
209
+ foreach ($r in $roots) {
210
+ if ((Test-Path (Join-Path $r "extensions")) -and (Test-Path (Join-Path $r "AGENTS.md"))) { return $r }
211
+ if (Test-Path (Join-Path $r "agent/extensions")) { return (Join-Path $r "agent") }
212
+ }
213
+ if ($Local) { throw "Local checkout not found (tried: $($roots -join ', '))" }
214
+ }
215
+ # Clone to ~/.pi/tmp — simple git clone, no account required, then copy agent folder.
216
+ # Use a unique subdir per run so concurrent installs never clobber each other.
217
+ if (-not (Test-Path $PiTmp)) { New-Item -ItemType Directory -Path $PiTmp -Force | Out-Null }
218
+ $tmp = Join-Path $PiTmp ("kiln-" + [Guid]::NewGuid().ToString("N").Substring(0,8))
219
+ New-Item -ItemType Directory -Path $tmp -Force | Out-Null
220
+ Write-Line "${cdim} → cloning $Repo (branch $Branch) → $tmp${creset}"
221
+ $env:GIT_TERMINAL_PROMPT = "0"; $env:GIT_ASKPASS = "echo"; $env:GCM_INTERACTIVE = "never"
222
+ $code = Invoke-NativeWithSpinner -Step "3" -Label "fetching config" -FilePath "git" -ArgumentList @("clone","--depth","1","--branch",$Branch,$Repo,$tmp)
223
+ if ($code -ne 0) { throw "git clone failed (exit $code). See $TmpLog" }
224
+ $script:ClonedTmp = $tmp
225
+ if (Test-Path (Join-Path $tmp "agent")) { return (Join-Path $tmp "agent") }
226
+ return $tmp
227
+ }
228
+
229
+ function Resolve-Npm {
230
+ # "npm" resolves to npm.ps1 (a PowerShell shim) under Windows PowerShell.
231
+ # npm.ps1 re-parses its own invocation line, which mangles arguments
232
+ # passed via variables/splatting — always prefer the native npm.cmd.
233
+ $cmd = Get-Command "npm.cmd" -ErrorAction SilentlyContinue
234
+ if ($cmd) { return $cmd.Source }
235
+ return "npm"
236
+ }
237
+
238
+ function Test-Node {
239
+ $node = Get-Command node -ErrorAction SilentlyContinue
240
+ if (-not $node) { return $null }
241
+ try { $v = (node --version 2>$null).Trim().TrimStart('v').Split('-')[0]; return [version]$v } catch { return $null }
242
+ }
243
+
244
+ # ── MAIN ─────────────────────────────────────────────────────────────────────
245
+ "" | Set-Content $TmpLog
246
+ # Cleanup trap
247
+ $null = Register-EngineEvent PowerShell.Exiting -Action { if ($script:ClonedTmp -and (Test-Path $script:ClonedTmp)) { Remove-Item $script:ClonedTmp -Recurse -Force -ErrorAction SilentlyContinue } } -ErrorAction SilentlyContinue
248
+
249
+ Write-Title
250
+ $targetDir = Resolve-Target
251
+ $repoUrl = Resolve-RepoUrl
252
+ $ok = $true
253
+
254
+ Write-Line "${cdim} target ${creset}$targetDir"
255
+ Write-Line "${cdim} repo ${creset}$repoUrl ${cdim}($Branch)${creset}"
256
+ Write-Line "${cdim} log ${creset}$TmpLog`n"
257
+
258
+ # ── preflight ────────────────────────────────────────────────────────────────
259
+ $nodeVer = Test-Node
260
+ if (-not $nodeVer) {
261
+ Write-Line "${cred} ✖ Node.js not found. Install Node 22.19+ from https://nodejs.org${creset}"
262
+ Write-Line " then re-run this installer.`n"
263
+ exit 1
264
+ }
265
+ if ($nodeVer -lt $NodeMinimum) {
266
+ Write-Line "${cyellow} ⚠ Node $nodeVer < $NodeMinimum — please upgrade to Node 22.19+${creset}"
267
+ if (-not $Yes) { Write-Line " continue anyway in 3s…"; Start-Sleep 3 }
268
+ }
269
+ if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
270
+ Write-Line "${cred} ✖ npm not found (Node installed but npm missing)${creset}"; exit 1
271
+ }
272
+ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
273
+ Write-Line "${cred} ✖ git not found — required to fetch config${creset}"
274
+ Write-Line " install from https://git-scm.com/downloads`n"; exit 1
275
+ }
276
+ $npmVer = (npm --version 2>$null).Trim()
277
+ $gitVer = (git --version 2>$null).Trim()
278
+ Write-Line "${cdim} node $nodeVer · npm $npmVer · $gitVer${creset}`n"
279
+ $NpmBin = Resolve-Npm
280
+
281
+ # ── [1/4] pi ────────────────────────────────────────────────────────────────
282
+ if ($SkipPi) {
283
+ Write-Line " ${cgreen}✔${creset} ${cdim}[1/4]${creset} Install pi ${cdim}— skipped${creset}"
284
+ } else {
285
+ $hasPi = $null -ne (Get-Command pi -ErrorAction SilentlyContinue)
286
+ $label = if ($hasPi) { "updating pi ($PiPackage)" } else { "installing pi ($PiPackage)" }
287
+ $code = Invoke-NativeWithSpinner -Step "1" -Label $label -FilePath $NpmBin -ArgumentList @("install","-g","$PiPackage@latest","--no-audit","--no-fund","--min-release-age=0")
288
+ if ($code -eq 0) {
289
+ $piVer = try { (pi --version 2>$null).Trim() } catch { "?" }
290
+ Write-Line " ${cgreen}✔${creset} ${cdim}[1/4]${creset} $label ${cdim}— $piVer${creset}"
291
+ } else {
292
+ $isPerm = Select-String -Path $TmpLog -Pattern "EACCES|permission" -Quiet -ErrorAction SilentlyContinue
293
+ if ($isPerm) {
294
+ Write-Line " ${cred}✖${creset} ${cdim}[1/4]${creset} $label ${cred}permission denied${creset}"
295
+ Write-Line " ${cdim}try: npm config set prefix ~/.npm-global or run as admin${creset}"
296
+ } else {
297
+ Write-Line " ${cred}✖${creset} ${cdim}[1/4]${creset} $label ${cred}failed (exit $code)${creset} ${cdim}see $TmpLog${creset}"
298
+ }
299
+ $ok = $false
300
+ }
301
+ }
302
+
303
+ # ── [2/4] pi packages ─────────────────────────────────────────────────────
304
+ if ($SkipPackages) {
305
+ Write-Line " ${cgreen}✔${creset} ${cdim}[2/4]${creset} pi packages ${cdim}— skipped${creset}"
306
+ } else {
307
+ $label = "installing pi-context-usage + @baretread/pi-forge"
308
+ $code = Invoke-NativeWithSpinner -Step "2" -Label $label -FilePath $NpmBin -ArgumentList @("install","-g","pi-context-usage@latest","@baretread/pi-forge@latest","--no-audit","--no-fund")
309
+ if ($code -eq 0) {
310
+ # Verify global ls (handles mise/nvm prefix quirks)
311
+ $hasCtx = $null -ne (npm ls -g pi-context-usage 2>$null | Select-String "pi-context-usage")
312
+ $hasForge = $null -ne (npm ls -g "@baretread/pi-forge" 2>$null | Select-String "pi-forge")
313
+ if (-not $hasCtx -or -not $hasForge) {
314
+ Write-Line " ${cyellow}⚠${creset} ${cdim}[2/4]${creset} pi packages installed but not in global ls — continuing${creset}"
315
+ }
316
+ Write-Line " ${cgreen}✔${creset} ${cdim}[2/4]${creset} pi packages ${cdim}— done${creset}"
317
+ } else {
318
+ Write-Line " ${cyellow}⚠${creset} ${cdim}[2/4]${creset} $label ${cyellow}exit $code — continuing (pi will auto-install)${creset}"
319
+ Write-Line " ${cdim}see $TmpLog${creset}"
320
+ }
321
+ }
322
+
323
+ # ── [3/4] custom config ───────────────────────────────────────────────────
324
+ try {
325
+ $sourceRoot = Resolve-SourceRoot -Repo $repoUrl
326
+ Write-Line "${cdim} source $sourceRoot${creset}"
327
+ if (-not (Test-Path $sourceRoot)) { throw "source not found: $sourceRoot" }
328
+
329
+ if (-not (Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }
330
+
331
+ # backup (managed files only: extensions + top-level config files).
332
+ # Per-user data (settings.json, auth.json, sessions/, taste/, etc.) is never
333
+ # touched by the installer, so it is deliberately NOT backed up.
334
+ $hasExisting = (Test-Path (Join-Path $targetDir "AGENTS.md")) -or (Test-Path (Join-Path $targetDir "extensions"))
335
+ if ($hasExisting) {
336
+ $bak = "$targetDir.bak.$(Get-Date -Format yyyyMMdd-HHmmss)"
337
+ Write-Line "${cdim} backup $targetDir/extensions → $bak${creset}"
338
+ New-Item -ItemType Directory -Path $bak -Force | Out-Null
339
+ foreach ($f in @("AGENTS.md","keybindings.json","README.md")) {
340
+ $s = Join-Path $targetDir $f
341
+ if (Test-Path $s) { Copy-Item $s (Join-Path $bak $f) -Force -ErrorAction SilentlyContinue }
342
+ }
343
+ $srcBakExt = Join-Path $targetDir "extensions"
344
+ if (Test-Path $srcBakExt) {
345
+ # NOTE: Copy-Item -Exclude only filters top-level items, not recursive
346
+ # contents — copy per-extension while skipping node_modules/.git instead.
347
+ $dstBakExt = Join-Path $bak "extensions"
348
+ Get-ChildItem $srcBakExt -Directory | ForEach-Object {
349
+ $d = Join-Path $dstBakExt $_.Name
350
+ New-Item -ItemType Directory -Path $d -Force | Out-Null
351
+ Get-ChildItem $_.FullName -Exclude @("node_modules",".git","bin") | ForEach-Object {
352
+ Copy-Item $_.FullName $d -Recurse -Force -ErrorAction SilentlyContinue
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ $copied = 0
359
+ $isSelfInstall = ($sourceRoot -eq $targetDir)
360
+ if ($isSelfInstall) { Write-Line "${cdim} source == target — skipping file copy (self-install)${creset}" }
361
+ foreach ($f in @("AGENTS.md","keybindings.json","README.md")) {
362
+ $s = Join-Path $sourceRoot $f
363
+ if (-not (Test-Path $s)) { continue }
364
+ $d = Join-Path $targetDir $f
365
+ if ($isSelfInstall -or $s -eq $d) { $copied++; continue }
366
+ Copy-Item $s $d -Force; $copied++
367
+ }
368
+ # settings.json: seed from repo defaults if missing; an existing file is never modified.
369
+ $repoSettings = Join-Path $sourceRoot "settings.json"
370
+ $settings = Join-Path $targetDir "settings.json"
371
+ $settingsJustCreated = $false
372
+ if ((-not (Test-Path $settings)) -and (Test-Path $repoSettings)) {
373
+ Copy-Item $repoSettings $settings
374
+ $settingsJustCreated = $true
375
+ Write-Line "${cdim} created settings.json from repo defaults${creset}"
376
+ } elseif (Test-Path $settings) {
377
+ Write-Line "${cdim} kept existing settings.json${creset}"
378
+ }
379
+ # ensure Pi extensions (forge + context) are registered — only on a freshly
380
+ # seeded settings.json; an existing file is left completely untouched.
381
+ if ($settingsJustCreated -and (Test-Path $settings) -and (Get-Command node -ErrorAction SilentlyContinue)) {
382
+ try {
383
+ $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){}"
384
+ node -e $patch $settings 2>$null | ForEach-Object { Write-Line "${cdim} $_${creset}" }
385
+ } catch {}
386
+ }
387
+ # taste.md / taste/ — user-specific, preserve if exists
388
+ foreach ($preserve in @("taste.md","taste","taste.json")) {
389
+ $p = Join-Path $targetDir $preserve
390
+ $src = Join-Path $sourceRoot $preserve
391
+ if (Test-Path $p) {
392
+ Write-Line "${cdim} kept existing $preserve${creset}"
393
+ } elseif (Test-Path $src) {
394
+ Copy-Item $src $p -Recurse -Force -Exclude @("node_modules",".git") -ErrorAction SilentlyContinue
395
+ }
396
+ }
397
+
398
+ # extensions — skip stray .md files, handle spaces
399
+ $srcExt = Join-Path $sourceRoot "extensions"
400
+ $dstExt = Join-Path $targetDir "extensions"
401
+ if (Test-Path $srcExt) {
402
+ if ($isSelfInstall -or $srcExt -eq $dstExt) {
403
+ # Self-install: extensions already in place — just count, don't delete/re-copy
404
+ Get-ChildItem $srcExt -Directory | Where-Object { $_.Name -notlike "*.md" } | ForEach-Object { $copied++ }
405
+ } else {
406
+ if (-not (Test-Path $dstExt)) { New-Item -ItemType Directory -Path $dstExt -Force | Out-Null }
407
+ Get-ChildItem $srcExt -Directory | ForEach-Object {
408
+ if ($_.Name -like "*.md") { return }
409
+ $dst = Join-Path $dstExt $_.Name
410
+ if (Test-Path $dst) { Remove-Item $dst -Recurse -Force -ErrorAction SilentlyContinue }
411
+ # NOTE: Copy-Item -Exclude only filters top-level items, not recursive
412
+ # contents — copy per-item while skipping node_modules/.git instead.
413
+ # (node_modules are rebuilt by each extension's installer in step 4;
414
+ # copying them follows junctions and stalls the install.)
415
+ New-Item -ItemType Directory -Path $dst -Force | Out-Null
416
+ Get-ChildItem $_.FullName -Exclude @("node_modules",".git") | ForEach-Object {
417
+ Copy-Item $_.FullName $dst -Recurse -Force -ErrorAction SilentlyContinue
418
+ }
419
+ $copied++
420
+ }
421
+ }
422
+ }
423
+
424
+ # themes — exclude recursive .pi and nul device file
425
+ $srcThemes = Join-Path $sourceRoot "themes"
426
+ if ((Test-Path $srcThemes) -and -not $isSelfInstall -and $srcThemes -ne (Join-Path $targetDir "themes")) {
427
+ $dstThemes = Join-Path $targetDir "themes"
428
+ if (Test-Path $dstThemes) { Remove-Item $dstThemes -Recurse -Force -ErrorAction SilentlyContinue }
429
+ New-Item -ItemType Directory -Path $dstThemes -Force | Out-Null
430
+ Get-ChildItem $srcThemes -Force | Where-Object { $_.Name -notin @(".pi","nul") } | ForEach-Object {
431
+ Copy-Item $_.FullName (Join-Path $dstThemes $_.Name) -Recurse -Force
432
+ }
433
+ }
434
+
435
+ Write-Line " ${cgreen}✔${creset} ${cdim}[3/4]${creset} custom config ${cdim}— $copied items → $targetDir${creset}"
436
+ # cleanup cloned temp — per spec: clone into ~/.pi/tmp, copy agent, delete temp
437
+ if ($script:ClonedTmp -and (Test-Path $script:ClonedTmp)) {
438
+ if ($script:ClonedTmp.StartsWith($PiTmp, [StringComparison]::OrdinalIgnoreCase)) {
439
+ Remove-Item $script:ClonedTmp -Recurse -Force -ErrorAction SilentlyContinue
440
+ $script:ClonedTmp = $null
441
+ }
442
+ }
443
+ # repo README → alongside the agent dir (e.g. ~/.pi/README.md)
444
+ $repoRoot = Split-Path $sourceRoot -Parent
445
+ $targetParent = Split-Path $targetDir -Parent
446
+ $repoReadme = Join-Path $repoRoot "README.md"
447
+ if ((Test-Path $repoReadme) -and ($repoRoot -ne $targetParent)) {
448
+ Copy-Item $repoReadme (Join-Path $targetParent "README.md") -Force -ErrorAction SilentlyContinue
449
+ }
450
+ } catch {
451
+ Write-Line " ${cred}✖${creset} ${cdim}[3/4]${creset} custom config ${cred}failed:${creset} $($_.Exception.Message)"
452
+ Write-Line " ${cdim}$($_.ScriptStackTrace)${creset}"
453
+ Write-Line " ${cdim}see $TmpLog${creset}"
454
+ $ok = $false
455
+ }
456
+
457
+ # ── [4/4] per-extension deps ──────────────────────────────────────────────
458
+ try {
459
+ $extRoot = Join-Path $targetDir "extensions"
460
+ $exts = @()
461
+ if (Test-Path $extRoot) { $exts = Get-ChildItem $extRoot -Directory | Where-Object { $_.Name -notlike "*.md" } | Sort-Object Name }
462
+
463
+ if ($exts.Count -eq 0) {
464
+ Write-Line " ${cyellow}⚠${creset} ${cdim}[4/4]${creset} no extensions found at $extRoot"
465
+ } else {
466
+ $failed = @()
467
+ $installed = 0
468
+ foreach ($ext in $exts) {
469
+ $label = "extensions/$($ext.Name)"
470
+ $ps1 = Join-Path $ext.FullName "install.ps1"
471
+ $sh = Join-Path $ext.FullName "install.sh"
472
+ $hasPkg = Test-Path (Join-Path $ext.FullName "package.json")
473
+
474
+ if (-not $hasPkg -and -not (Test-Path $ps1)) {
475
+ Write-Line " ${cdim} · $label — no package.json, skipping${creset}"
476
+ continue
477
+ }
478
+
479
+ if (Test-Path $ps1) {
480
+ # prefer pwsh, fallback to powershell
481
+ $pwsh = Get-Command pwsh -ErrorAction SilentlyContinue
482
+ if ($pwsh) { $code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath $pwsh.Source -ArgumentList @("-NoProfile","-File",$ps1) }
483
+ else { $code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath "powershell" -ArgumentList @("-NoProfile","-ExecutionPolicy","Bypass","-File",$ps1) }
484
+ } elseif (Test-Path $sh) {
485
+ $code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath "sh" -ArgumentList @($sh)
486
+ } else {
487
+ $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") }
488
+ $code = Invoke-NativeWithSpinner -Step "4" -Label $label -FilePath $NpmBin -ArgumentList $npmArgs
489
+ }
490
+
491
+ if ($code -eq 0) {
492
+ Write-Line " ${cgreen}✔${creset} ${cdim}[4/4]${creset} $label"
493
+ $installed++
494
+ } else {
495
+ Write-Line " ${cred}✖${creset} ${cdim}[4/4]${creset} $label ${cred}failed (exit $code)${creset}"
496
+ $failed += $ext.Name
497
+ }
498
+ }
499
+ if ($failed.Count -eq 0) {
500
+ Write-Line " ${cgreen}✔${creset} ${cdim}[4/4]${creset} extension deps ${cdim}— $installed installed${creset}"
501
+ } else {
502
+ Write-Line " ${cyellow}⚠${creset} ${cdim}[4/4]${creset} extension deps ${cyellow}$($failed.Count) failed:${creset} $($failed -join ', ') ${cdim}see $TmpLog${creset}"
503
+ }
504
+ }
505
+ } catch {
506
+ Write-Line " ${cred}✖${creset} ${cdim}[4/4]${creset} extension deps ${cred}failed:${creset} $($_.Exception.Message)"
507
+ $ok = $false
508
+ }
509
+
510
+ # ── cleanup + summary ─────────────────────────────────────────────────────
511
+ if ($script:ClonedTmp -and (Test-Path $script:ClonedTmp)) {
512
+ Remove-Item $script:ClonedTmp -Recurse -Force -ErrorAction SilentlyContinue
513
+ }
514
+
515
+ $elapsed = (Get-Date) - $script:StartTime
516
+ if ($elapsed.TotalSeconds -ge 60) { $elapsedFmt = "{0}m {1}s" -f [int]$elapsed.TotalMinutes, $elapsed.Seconds }
517
+ else { $elapsedFmt = "{0}s" -f [int]$elapsed.TotalSeconds }
518
+
519
+ Write-Line ""
520
+ if ($ok) {
521
+ Write-Line "${cgreen}${cbold} ✓ Setup complete${creset} ${cdim}→ $targetDir · Kiln installed in $elapsedFmt${creset}"
522
+ $piVer2 = try { (pi --version 2>$null).Trim() } catch { $null }
523
+ if ($piVer2) { Write-Line "${cdim} pi $piVer2 · run: pi${creset}" }
524
+ } else {
525
+ Write-Line "${cyellow}${cbold} ⚠ Setup finished with warnings${creset} ${cdim}in $elapsedFmt · see $TmpLog${creset}"
526
+ }
527
+ Write-Line "${cdim} log: $TmpLog${creset}`n"