@posthog/wizard 2.80.0 → 2.81.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 (228) hide show
  1. package/README.md +204 -72
  2. package/dist/{PickerMenu-m4CCPX4f.js → PickerMenu-BGGeYqd-.js} +104 -4
  3. package/dist/PickerMenu-BGGeYqd-.js.map +1 -0
  4. package/dist/README.md +13 -8
  5. package/dist/{agent-interface-CzyCmp7q.js → agent-interface-BFzBnz10.js} +289 -361
  6. package/dist/agent-interface-BFzBnz10.js.map +1 -0
  7. package/dist/{agent-prompt-loader-4wMxlYSX.js → agent-prompt-loader-CbCW6YbP.js} +6 -5
  8. package/dist/agent-prompt-loader-CbCW6YbP.js.map +1 -0
  9. package/dist/analytics-CGGE7OSa.js +735 -0
  10. package/dist/analytics-CGGE7OSa.js.map +1 -0
  11. package/dist/analytics-D6NAIJxP.js +2 -0
  12. package/dist/api-DM4uMJ00.js +2 -0
  13. package/dist/{api-C3Dc4LFl.js → api-ZZhiRtbb.js} +3 -3
  14. package/dist/api-ZZhiRtbb.js.map +1 -0
  15. package/dist/api-key-login-C30LRht2.js +2 -0
  16. package/dist/api-key-login-D0KBr1dx.js +55 -0
  17. package/dist/api-key-login-D0KBr1dx.js.map +1 -0
  18. package/dist/bin.js +2 -2
  19. package/dist/bin.js.map +1 -1
  20. package/dist/{bounded-fs-CC_NDtoP.js → bounded-fs-DwL2Tasp.js} +3 -3
  21. package/dist/{bounded-fs-CC_NDtoP.js.map → bounded-fs-DwL2Tasp.js.map} +1 -1
  22. package/dist/{ci-install-DfcAMhlu.js → ci-install-DKXrt_ow.js} +21 -25
  23. package/dist/ci-install-DKXrt_ow.js.map +1 -0
  24. package/dist/console-log-DJiTqYT2.js +32 -0
  25. package/dist/console-log-DJiTqYT2.js.map +1 -0
  26. package/dist/{debug-DyARf5TF.js → debug-sB20p-Ek.js} +18 -52
  27. package/dist/debug-sB20p-Ek.js.map +1 -0
  28. package/dist/{defaults-DpfMnH2q.js → defaults-DDVl3tKN.js} +2 -51
  29. package/dist/defaults-DDVl3tKN.js.map +1 -0
  30. package/dist/{emit-BSetXzDV.js → emit-BqLCdV8Y.js} +2 -1
  31. package/dist/{emit-BSetXzDV.js.map → emit-BqLCdV8Y.js.map} +1 -1
  32. package/dist/{errors-C2domSkK.js → errors-DhFeR8Sp.js} +5 -5
  33. package/dist/{errors-C2domSkK.js.map → errors-DhFeR8Sp.js.map} +1 -1
  34. package/dist/family-picker-BKs-tTyu.js +46 -0
  35. package/dist/family-picker-BKs-tTyu.js.map +1 -0
  36. package/dist/{gateway-BmjYUSua.js → gateway-Dg4Fm-nC.js} +122 -11
  37. package/dist/gateway-Dg4Fm-nC.js.map +1 -0
  38. package/dist/headless-B3Ol1nkU.js +3 -0
  39. package/dist/headless-DhTDC9ga.js +10 -0
  40. package/dist/headless-DhTDC9ga.js.map +1 -0
  41. package/dist/{host-resolution-DVSb7kXb.js → host-resolution-DDYJXiAQ.js} +4 -4
  42. package/dist/{host-resolution-DVSb7kXb.js.map → host-resolution-DDYJXiAQ.js.map} +1 -1
  43. package/dist/install-CZeR38cD.js +2 -0
  44. package/dist/{install-Bk5TNk27.js → install-Dh8Z5R15.js} +62 -143
  45. package/dist/install-Dh8Z5R15.js.map +1 -0
  46. package/dist/interactive-hqHk6kz4.js +11 -0
  47. package/dist/interactive-hqHk6kz4.js.map +1 -0
  48. package/dist/{links-9LWZPSCq.js → links-rMot83W2.js} +3 -3
  49. package/dist/{links-9LWZPSCq.js.map → links-rMot83W2.js.map} +1 -1
  50. package/dist/{main-DKPjFHEV.js → main-DNIeasXx.js} +205 -470
  51. package/dist/main-DNIeasXx.js.map +1 -0
  52. package/dist/{mcp-Co8VRF62.js → mcp-SZcGfaOz.js} +3 -4
  53. package/dist/{mcp-Co8VRF62.js.map → mcp-SZcGfaOz.js.map} +1 -1
  54. package/dist/mcp-install-smoke-test.ts +2 -2
  55. package/dist/{mcp-prompt-streaming-ljCVoQ9I.js → mcp-prompt-streaming-BuRMBgbo.js} +12 -12
  56. package/dist/mcp-prompt-streaming-BuRMBgbo.js.map +1 -0
  57. package/dist/mode-label-DB3ol_sj.js +9 -0
  58. package/dist/mode-label-DB3ol_sj.js.map +1 -0
  59. package/dist/{models-Dba8ITeo.js → models-G1j-wgwe.js} +2 -2
  60. package/dist/{models-Dba8ITeo.js.map → models-G1j-wgwe.js.map} +1 -1
  61. package/dist/non-interactive-DkldwWDM.js +18 -0
  62. package/dist/non-interactive-DkldwWDM.js.map +1 -0
  63. package/dist/{oauth-session-D-RfcVmr.js → oauth-session-C2fVSGfr.js} +3 -3
  64. package/dist/{oauth-session-D-RfcVmr.js.map → oauth-session-C2fVSGfr.js.map} +1 -1
  65. package/dist/{orchestrator-tools-CVJ1F9Ub.js → orchestrator-tools-BchqG7Qi.js} +3 -3
  66. package/dist/{orchestrator-tools-CVJ1F9Ub.js.map → orchestrator-tools-BchqG7Qi.js.map} +1 -1
  67. package/dist/{package-json-Cowya9-3.js → package-json-2UrhE6kP.js} +1 -1
  68. package/dist/{package-json-Cowya9-3.js.map → package-json-2UrhE6kP.js.map} +1 -1
  69. package/dist/package-json-Dblip2_X.js +2 -0
  70. package/dist/package-manager-B3_qSybg.js +105 -0
  71. package/dist/package-manager-B3_qSybg.js.map +1 -0
  72. package/dist/{pi-B5YTBRDv.js → pi-Cw7CPXSl.js} +17 -19
  73. package/dist/pi-Cw7CPXSl.js.map +1 -0
  74. package/dist/playground-CPNrQfb3.js +12 -0
  75. package/dist/playground-CPNrQfb3.js.map +1 -0
  76. package/dist/posthog-integration-FAWfClH3.js +10304 -0
  77. package/dist/posthog-integration-FAWfClH3.js.map +1 -0
  78. package/dist/programs-DjCmEcxb.js +3 -0
  79. package/dist/programs-Ml6OLybj.js +5402 -0
  80. package/dist/programs-Ml6OLybj.js.map +1 -0
  81. package/dist/{provisioning-CLhuKqXH.js → provisioning-BTVL49jD.js} +4 -5
  82. package/dist/{provisioning-CLhuKqXH.js.map → provisioning-BTVL49jD.js.map} +1 -1
  83. package/dist/provisioning-DL0xCJXH.js +2 -0
  84. package/dist/{queue-tools-knnPQP0e.js → queue-tools-BQfRI_uD.js} +6 -8
  85. package/dist/queue-tools-BQfRI_uD.js.map +1 -0
  86. package/dist/results-ycm_Tf31.js +90 -0
  87. package/dist/results-ycm_Tf31.js.map +1 -0
  88. package/dist/run-Bgg-ctbA.js +264 -0
  89. package/dist/run-Bgg-ctbA.js.map +1 -0
  90. package/dist/run-C6Ei6vvb.js +246 -0
  91. package/dist/run-C6Ei6vvb.js.map +1 -0
  92. package/dist/run-tool-CrN2xWzU.js +2436 -0
  93. package/dist/run-tool-CrN2xWzU.js.map +1 -0
  94. package/dist/run-tool-tNjm8S-E.js +2 -0
  95. package/dist/runners-GGYJs0Ny.js +271 -0
  96. package/dist/runners-GGYJs0Ny.js.map +1 -0
  97. package/dist/{security-BvdSFDuM.js → security-C36p8lu6.js} +44 -52
  98. package/dist/security-C36p8lu6.js.map +1 -0
  99. package/dist/setup-utils-Dfgse6pY.js +118 -0
  100. package/dist/setup-utils-Dfgse6pY.js.map +1 -0
  101. package/dist/smoke-test.sh +1 -1
  102. package/dist/{playground-BLsCfHmQ.js → start-playground-D0TrdLmc.js} +44 -34
  103. package/dist/start-playground-D0TrdLmc.js.map +1 -0
  104. package/dist/{subagent-D_Sqak94.js → subagent-DR5caRzL.js} +7 -7
  105. package/dist/subagent-DR5caRzL.js.map +1 -0
  106. package/dist/{task-DiTI6Sak.js → task-C1qKgsAf.js} +18 -18
  107. package/dist/task-C1qKgsAf.js.map +1 -0
  108. package/dist/{tasks-ChZ_F1YP.js → tasks-BfJhVky2.js} +1 -1
  109. package/dist/tasks-BfJhVky2.js.map +1 -0
  110. package/dist/{telemetry-BNKsDxNS.js → telemetry-Ch9ZJvm7.js} +2 -2
  111. package/dist/{telemetry-BNKsDxNS.js.map → telemetry-Ch9ZJvm7.js.map} +1 -1
  112. package/dist/terminal-D20JFN3J.js +16188 -0
  113. package/dist/terminal-D20JFN3J.js.map +1 -0
  114. package/dist/tools-BZseGChD.js +544 -0
  115. package/dist/tools-BZseGChD.js.map +1 -0
  116. package/dist/{tools-C8LQ8QFz.js → tools-CmSMclOD.js} +9 -9
  117. package/dist/tools-CmSMclOD.js.map +1 -0
  118. package/dist/tui-BXe1D0tc.js +25 -0
  119. package/dist/tui-BXe1D0tc.js.map +1 -0
  120. package/dist/tui-DJf3ZPRh.js +2 -0
  121. package/dist/{steps-Hc28zVc_.js → upload-step-BlAN7MC5.js} +14 -16
  122. package/dist/upload-step-BlAN7MC5.js.map +1 -0
  123. package/dist/{urls-BI4kJ4qQ.js → urls-eFaChZ9d.js} +2 -2
  124. package/dist/{urls-BI4kJ4qQ.js.map → urls-eFaChZ9d.js.map} +1 -1
  125. package/dist/warlock-smoke-test.ts +1 -2
  126. package/dist/{wizard-abort-DBKtfnBR.js → wizard-abort-BX3O4Hg4.js} +40 -33
  127. package/dist/wizard-abort-BX3O4Hg4.js.map +1 -0
  128. package/dist/{yara-hooks-C3L6lBij.js → yara-hooks-C4HOysrM.js} +283 -21
  129. package/dist/yara-hooks-C4HOysrM.js.map +1 -0
  130. package/dist/yara-hooks-DV4EkkZY.js +2 -0
  131. package/package.json +7 -6
  132. package/dist/PickerMenu-m4CCPX4f.js.map +0 -1
  133. package/dist/agent-BkBbVuNq.js +0 -3189
  134. package/dist/agent-BkBbVuNq.js.map +0 -1
  135. package/dist/agent-QGK2YzlX.js +0 -6
  136. package/dist/agent-interface-CzyCmp7q.js.map +0 -1
  137. package/dist/agent-prompt-loader-4wMxlYSX.js.map +0 -1
  138. package/dist/analytics-B6q5PN7a.js +0 -301
  139. package/dist/analytics-B6q5PN7a.js.map +0 -1
  140. package/dist/analytics-BXtfK9o7.js +0 -2
  141. package/dist/api-C3Dc4LFl.js.map +0 -1
  142. package/dist/api-yNjrfZs2.js +0 -2
  143. package/dist/check-screens.tsx +0 -190
  144. package/dist/ci-install-DfcAMhlu.js.map +0 -1
  145. package/dist/constants-DO4X70po.js +0 -260
  146. package/dist/constants-DO4X70po.js.map +0 -1
  147. package/dist/debug-BueK5mRR.js +0 -2
  148. package/dist/debug-DyARf5TF.js.map +0 -1
  149. package/dist/defaults-DpfMnH2q.js.map +0 -1
  150. package/dist/endpoints-D-4KxZiB.js +0 -108
  151. package/dist/endpoints-D-4KxZiB.js.map +0 -1
  152. package/dist/env-api-key-CHGLe0iz.js +0 -2
  153. package/dist/env-api-key-COf0U-Au.js +0 -17
  154. package/dist/env-api-key-COf0U-Au.js.map +0 -1
  155. package/dist/environment-DrLaSsA8.js +0 -39
  156. package/dist/environment-DrLaSsA8.js.map +0 -1
  157. package/dist/environment-hBj7ngmO.js +0 -2
  158. package/dist/file-CVA9ocIC.js +0 -60
  159. package/dist/file-CVA9ocIC.js.map +0 -1
  160. package/dist/file-CmoKK_Pp.js +0 -2
  161. package/dist/file-watcher-C3Su8vXm.js +0 -123
  162. package/dist/file-watcher-C3Su8vXm.js.map +0 -1
  163. package/dist/gateway-BmjYUSua.js.map +0 -1
  164. package/dist/headless-ui-CUbv12Yx.js +0 -44
  165. package/dist/headless-ui-CUbv12Yx.js.map +0 -1
  166. package/dist/install-Bk5TNk27.js.map +0 -1
  167. package/dist/install-F4wm3RSl.js +0 -2
  168. package/dist/interactive-C5LNvDj6.js +0 -11
  169. package/dist/interactive-C5LNvDj6.js.map +0 -1
  170. package/dist/local-dev-McsurOC-.js +0 -135
  171. package/dist/local-dev-McsurOC-.js.map +0 -1
  172. package/dist/logging-ui-p6XKVmTx.js +0 -226
  173. package/dist/logging-ui-p6XKVmTx.js.map +0 -1
  174. package/dist/main-DKPjFHEV.js.map +0 -1
  175. package/dist/mcp-prompt-streaming-ljCVoQ9I.js.map +0 -1
  176. package/dist/mint-failure-C0HyvEwO.js +0 -6403
  177. package/dist/mint-failure-C0HyvEwO.js.map +0 -1
  178. package/dist/non-interactive-CLMJrj_7.js +0 -18
  179. package/dist/non-interactive-CLMJrj_7.js.map +0 -1
  180. package/dist/package-json--ppioh2F.js +0 -2
  181. package/dist/pi-B5YTBRDv.js.map +0 -1
  182. package/dist/playground-BLsCfHmQ.js.map +0 -1
  183. package/dist/posthog-B2E0hpZt.js +0 -2
  184. package/dist/posthog-B42rt_1S.js +0 -124
  185. package/dist/posthog-B42rt_1S.js.map +0 -1
  186. package/dist/project-data--L-WZyvj.js +0 -2
  187. package/dist/project-data-BO9IXn9y.js +0 -1131
  188. package/dist/project-data-BO9IXn9y.js.map +0 -1
  189. package/dist/provisioning-C01REIU2.js +0 -2
  190. package/dist/queue-tools-knnPQP0e.js.map +0 -1
  191. package/dist/registry-B3rC5U4b.js +0 -3464
  192. package/dist/registry-B3rC5U4b.js.map +0 -1
  193. package/dist/registry-BamsM7J9.js +0 -2
  194. package/dist/run-agent-legacy-CwUmPfQF.js +0 -2
  195. package/dist/run-agent-legacy-DWBajsMh.js +0 -3467
  196. package/dist/run-agent-legacy-DWBajsMh.js.map +0 -1
  197. package/dist/runners-D1PqqpjA.js +0 -834
  198. package/dist/runners-D1PqqpjA.js.map +0 -1
  199. package/dist/security-BvdSFDuM.js.map +0 -1
  200. package/dist/setup-utils-ChXXTi1m.js +0 -301
  201. package/dist/setup-utils-ChXXTi1m.js.map +0 -1
  202. package/dist/start-tui-DicLfgsj.js +0 -6964
  203. package/dist/start-tui-DicLfgsj.js.map +0 -1
  204. package/dist/steps-Hc28zVc_.js.map +0 -1
  205. package/dist/store-COCO6U0P.js +0 -5
  206. package/dist/store-D3509G87.js +0 -1030
  207. package/dist/store-D3509G87.js.map +0 -1
  208. package/dist/subagent-D_Sqak94.js.map +0 -1
  209. package/dist/task-DiTI6Sak.js.map +0 -1
  210. package/dist/task-status-CmrrR7zo.js +0 -16
  211. package/dist/task-status-CmrrR7zo.js.map +0 -1
  212. package/dist/task-stream-BckhG_cB.js +0 -363
  213. package/dist/task-stream-BckhG_cB.js.map +0 -1
  214. package/dist/tasks-ChZ_F1YP.js.map +0 -1
  215. package/dist/terminal-PK9bCLRi.js +0 -7222
  216. package/dist/terminal-PK9bCLRi.js.map +0 -1
  217. package/dist/tools-C8LQ8QFz.js.map +0 -1
  218. package/dist/ui-tPNpM6vj.js +0 -15
  219. package/dist/ui-tPNpM6vj.js.map +0 -1
  220. package/dist/version-5_ydHkM6.js +0 -6
  221. package/dist/version-5_ydHkM6.js.map +0 -1
  222. package/dist/wizard-abort-BVjqLyeA.js +0 -3
  223. package/dist/wizard-abort-DBKtfnBR.js.map +0 -1
  224. package/dist/wizard-session-B7BHnay0.js +0 -2
  225. package/dist/wizard-session-BYw6LzpS.js +0 -133
  226. package/dist/wizard-session-BYw6LzpS.js.map +0 -1
  227. package/dist/yara-hooks-C3L6lBij.js.map +0 -1
  228. package/dist/yara-hooks-CF_IsoaA.js +0 -2
@@ -0,0 +1,2436 @@
1
+ import { n as ErrorCodes, t as emitWizardError } from "./emit-BqLCdV8Y.js";
2
+ import { f as VERSION, i as getLogFilePath, o as logToFile } from "./debug-sB20p-Ek.js";
3
+ import { M as POSTHOG_DOCS_URL, O as OAUTH_PORTS, ct as getLocalDev, k as OAUTH_TIMEOUT_MS, n as analytics, rt as getSkillsBaseUrl, st as checkLocalServices, y as GITHUB_SKILLS_BASE_URL } from "./analytics-CGGE7OSa.js";
4
+ import { c as buildSession, it as SIGNUP_WIZARD_READINESS_CONFIG, k as getProgramConfig, m as logIn, ot as getBlockingServiceKeys } from "./programs-Ml6OLybj.js";
5
+ import { a as WizardError, t as classifyRunFailure } from "./errors-DhFeR8Sp.js";
6
+ import { r as readFileHead } from "./bounded-fs-DwL2Tasp.js";
7
+ import { n as printAbortOutro } from "./console-log-DJiTqYT2.js";
8
+ import { T as readNeedsAttention, m as runCleanups, w as NEEDS_ATTENTION_HEADING } from "./agent-interface-BFzBnz10.js";
9
+ import { A as formatCostUsd, T as scanInstalledSkill, j as formatTokenCount } from "./posthog-integration-FAWfClH3.js";
10
+ import { _ as downloadSkill, y as fetchSkillMenu } from "./yara-hooks-C4HOysrM.js";
11
+ import { n as namesWithStatus, r as redactSecrets, s as toClientResult } from "./results-ycm_Tf31.js";
12
+ import { c as flushAnalytics } from "./tools-BZseGChD.js";
13
+ import { t as ALL_FEATURE_VALUES } from "./defaults-DDVl3tKN.js";
14
+ import { n as startHostExit, r as wizardAbort } from "./wizard-abort-BX3O4Hg4.js";
15
+ import { a as installPlugins, i as getSupportedPluginClients, n as getInstalledClients, o as removeMCPServer, r as getSupportedClients, s as isPluginCapable } from "./install-Dh8Z5R15.js";
16
+ import { a as useStdoutDimensions, h as Icons, m as Colors, n as useKeyBindings, t as PickerMenu } from "./PickerMenu-BGGeYqd-.js";
17
+ import { B as totalTokenCount, G as ModalOverlay, H as LogViewer, I as HNViewer, J as ProgressList, K as ConfirmationInput, L as TabContainer, M as IntroScreenLayout, N as PRIVACY_PANEL_LABEL, P as PrivacyPanel, Q as getContentBlocks, R as ScreenContainer, T as McpScreen, U as LinkText, V as EventPlanViewer, W as extractUrls, X as SplitView, Y as LoadingBox, _ as getTuiTool, a as SlackConnectScreen, b as extractOAuthCode, c as LearnCard, d as MINT_FAILURE_BODY, f as MINT_FAILURE_CONTACT, g as listFlowOwners, h as flowOwner, i as OutroScreen, k as useDismissOnAnyKey, l as AiOptInRequiredScreen, m as isRunFailure, n as releaseTerminal, o as TipsCard, p as MINT_FAILURE_MESSAGE, r as WizardStore, s as VisualizerTab, t as enterDarkTerminal, u as ServiceHealthList, x as abortOnScreens, y as getOrAskForProjectData } from "./terminal-D20JFN3J.js";
18
+ import path, { join } from "path";
19
+ import { tmpdir } from "node:os";
20
+ import path$1, { join as join$1 } from "node:path";
21
+ import { spawn } from "node:child_process";
22
+ import { constants } from "node:fs";
23
+ import fs from "fs/promises";
24
+ import { access, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
25
+ import { Box, Text, render, useInput } from "ink";
26
+ import { createElement, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
27
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
28
+ import { PasswordInput, TextInput } from "@inkjs/ui";
29
+ //#region src/tui/screens/health/HealthCheckScreen.tsx
30
+ /**
31
+ * HealthCheckScreen — Program screen between Intro and Auth.
32
+ *
33
+ * Three states:
34
+ * 1. Checking: spinner while health check runs
35
+ * 2. Healthy: isComplete returns true, router auto-advances to Auth
36
+ * 3. Blocking outage: shows affected services with Continue/Exit
37
+ */
38
+ const EXAMPLE_PROMPT = "Integrate PostHog into this project using the skill files in .posthog/skills/. Read SKILL.md first, then follow the numbered program files in order.";
39
+ const SkillsDownloadedScreen = ({ store }) => {
40
+ useDismissOnAnyKey(() => store.requestExit(0));
41
+ return /* @__PURE__ */ jsxs(Box, {
42
+ flexDirection: "column",
43
+ flexGrow: 1,
44
+ children: [
45
+ /* @__PURE__ */ jsxs(Text, {
46
+ color: "green",
47
+ bold: true,
48
+ children: [Icons.check, " Skills downloaded to .posthog/skills/"]
49
+ }),
50
+ /* @__PURE__ */ jsxs(Box, {
51
+ marginTop: 1,
52
+ flexDirection: "column",
53
+ children: [/* @__PURE__ */ jsx(Text, { children: "You can continue setup with another agent using this prompt:" }), /* @__PURE__ */ jsx(Box, {
54
+ marginTop: 1,
55
+ paddingLeft: 2,
56
+ children: /* @__PURE__ */ jsx(Text, {
57
+ color: "cyan",
58
+ children: EXAMPLE_PROMPT
59
+ })
60
+ })]
61
+ }),
62
+ /* @__PURE__ */ jsx(Box, {
63
+ marginTop: 1,
64
+ children: /* @__PURE__ */ jsx(Text, {
65
+ color: Colors.muted,
66
+ children: "Press any key to exit"
67
+ })
68
+ })
69
+ ]
70
+ });
71
+ };
72
+ const HealthCheckScreen = ({ store }) => {
73
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
74
+ const [downloaded, setDownloaded] = useState(false);
75
+ const [downloading, setDownloading] = useState(false);
76
+ const result = store.session.readinessResult;
77
+ if (downloaded) return /* @__PURE__ */ jsx(SkillsDownloadedScreen, { store });
78
+ if (!result) return /* @__PURE__ */ jsx(Box, {
79
+ flexDirection: "column",
80
+ flexGrow: 1,
81
+ alignItems: "center",
82
+ justifyContent: "center",
83
+ children: /* @__PURE__ */ jsx(LoadingBox, { message: "Checking service status..." })
84
+ });
85
+ const isSignup = store.session.signup;
86
+ const blockingKeys = getBlockingServiceKeys(result.health, isSignup ? SIGNUP_WIZARD_READINESS_CONFIG : void 0);
87
+ const warningKeys = isSignup ? getBlockingServiceKeys(result.health).filter((k) => !blockingKeys.includes(k)) : [];
88
+ const hasHardBlock = blockingKeys.length > 0;
89
+ const displayKeys = hasHardBlock ? blockingKeys : warningKeys;
90
+ if (displayKeys.length === 0) return null;
91
+ const isSkillsOriginDown = hasHardBlock && blockingKeys.includes("skillsOrigin");
92
+ const canDownloadSkills = result.health.skillsOrigin.status === "healthy";
93
+ const integration = store.session.integration;
94
+ const allBlockingHaveNoConnection = hasHardBlock && displayKeys.every((k) => result.health[k].status === "no-connection");
95
+ const title = isSkillsOriginDown ? "Ongoing service disruptions" : allBlockingHaveNoConnection ? "Couldn't reach PostHog" : hasHardBlock ? "Ongoing service disruptions" : "Service disruption detected";
96
+ const docsUrl = store.session.frameworkConfig?.metadata.docsUrl;
97
+ const description = isSkillsOriginDown ? "The Wizard can't download the skills it needs — neither GitHub Releases nor PostHog's mirror is reachable right now." : allBlockingHaveNoConnection ? "We couldn't reach these services from this machine. PostHog's status page shows no incidents, so this is most likely a network issue — VPN, firewall, captive portal, or flaky Wi-Fi." : hasHardBlock ? "The Wizard cannot start while these services are down." : "Some services are degraded. You can continue, but parts of the wizard may not work reliably.";
98
+ const handleDownloadAndExit = async () => {
99
+ if (downloading) return;
100
+ setDownloading(true);
101
+ const menu = await fetchSkillMenu(GITHUB_SKILLS_BASE_URL);
102
+ if (menu) {
103
+ const prefix = `integration-${integration}`;
104
+ const skills = (menu.categories["integration"] ?? []).filter((s) => s.id.startsWith(prefix));
105
+ for (const skill of skills) await downloadSkill(skill, store.session.installDir, {
106
+ skillsRoot: ".posthog/skills",
107
+ scan: (dir) => scanInstalledSkill(dir, void 0)
108
+ });
109
+ }
110
+ setDownloaded(true);
111
+ };
112
+ return /* @__PURE__ */ jsxs(ModalOverlay, {
113
+ borderColor: hasHardBlock && !allBlockingHaveNoConnection ? "red" : "yellow",
114
+ title,
115
+ width: 72,
116
+ footer: isSkillsOriginDown ? /* @__PURE__ */ jsx(ConfirmationInput, {
117
+ message: "",
118
+ confirmLabel: "",
119
+ cancelLabel: "Exit [Esc]",
120
+ onConfirm: () => void abortOnScreens(store, {
121
+ code: ErrorCodes.EnvServiceOutage,
122
+ message: "Exited due to service outage."
123
+ }),
124
+ onCancel: () => void abortOnScreens(store, {
125
+ code: ErrorCodes.EnvServiceOutage,
126
+ message: "Exited due to service outage."
127
+ })
128
+ }) : /* @__PURE__ */ jsx(ConfirmationInput, {
129
+ message: "Continue anyway?",
130
+ confirmLabel: "Continue [Enter]",
131
+ cancelLabel: canDownloadSkills && !isSkillsOriginDown ? downloading ? "Downloading..." : "Download skills & Exit [Esc]" : "Exit [Esc]",
132
+ onConfirm: () => store.dismissOutage(),
133
+ onCancel: canDownloadSkills && !isSkillsOriginDown ? () => void handleDownloadAndExit() : () => void abortOnScreens(store, {
134
+ code: ErrorCodes.EnvServiceOutage,
135
+ message: "Exited due to service outage."
136
+ })
137
+ }),
138
+ children: [
139
+ /* @__PURE__ */ jsxs(Box, {
140
+ flexDirection: "column",
141
+ marginBottom: 1,
142
+ children: [/* @__PURE__ */ jsx(Box, {
143
+ marginBottom: 1,
144
+ children: /* @__PURE__ */ jsxs(Text, { children: [
145
+ /* @__PURE__ */ jsx(Text, {
146
+ color: "red",
147
+ children: Icons.squareFilled
148
+ }),
149
+ /* @__PURE__ */ jsx(Text, {
150
+ dimColor: true,
151
+ children: " Down "
152
+ }),
153
+ /* @__PURE__ */ jsx(Text, {
154
+ color: "#DC9300",
155
+ children: Icons.squareFilled
156
+ }),
157
+ /* @__PURE__ */ jsx(Text, {
158
+ dimColor: true,
159
+ children: " Degraded "
160
+ }),
161
+ /* @__PURE__ */ jsx(Text, {
162
+ color: "gray",
163
+ children: Icons.squareFilled
164
+ }),
165
+ /* @__PURE__ */ jsx(Text, {
166
+ dimColor: true,
167
+ children: " No connection"
168
+ })
169
+ ] })
170
+ }), /* @__PURE__ */ jsx(ServiceHealthList, {
171
+ health: result.health,
172
+ filterKeys: displayKeys,
173
+ showHealthy: false
174
+ })]
175
+ }),
176
+ /* @__PURE__ */ jsx(Text, {
177
+ dimColor: true,
178
+ children: description
179
+ }),
180
+ isSkillsOriginDown && docsUrl && /* @__PURE__ */ jsx(Box, {
181
+ marginTop: 1,
182
+ children: /* @__PURE__ */ jsxs(Text, { children: ["Set up manually: ", /* @__PURE__ */ jsx(Text, {
183
+ color: "cyan",
184
+ children: docsUrl
185
+ })] })
186
+ }),
187
+ canDownloadSkills && !isSkillsOriginDown && /* @__PURE__ */ jsx(Box, {
188
+ marginTop: 1,
189
+ children: /* @__PURE__ */ jsx(Text, { children: "You can still download the PostHog integration skills and continue with another agent." })
190
+ })
191
+ ]
192
+ });
193
+ };
194
+ //#endregion
195
+ //#region src/tui/screens/SettingsOverrideScreen.tsx
196
+ const SettingsOverrideScreen = ({ store }) => {
197
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
198
+ const [feedback, setFeedback] = useState(null);
199
+ const conflicts = store.settingsConflicts?.filter((c) => c.writable);
200
+ const hasConflicts = Boolean(conflicts && conflicts.length > 0);
201
+ useEffect(() => {
202
+ if (hasConflicts) analytics.wizardCapture("settings conflict shown", { kind: "override" });
203
+ }, [hasConflicts]);
204
+ if (!conflicts || conflicts.length === 0) return null;
205
+ return /* @__PURE__ */ jsxs(ModalOverlay, {
206
+ borderColor: "red",
207
+ title: `${Icons.warning} Settings conflict`,
208
+ width: 64,
209
+ feedback: feedback ? `${Icons.warning} ${feedback}` : null,
210
+ footer: /* @__PURE__ */ jsx(ConfirmationInput, {
211
+ message: "Back up to .wizard-backup and continue?",
212
+ confirmLabel: "Backup & continue [Enter]",
213
+ cancelLabel: "Exit [Esc]",
214
+ onConfirm: () => {
215
+ analytics.wizardCapture("settings conflict accepted", { kind: "override" });
216
+ if (!store.backupAndFixSettingsOverride()) setFeedback("Could not back up the settings file.");
217
+ },
218
+ onCancel: () => store.requestExit(1)
219
+ }),
220
+ children: [conflicts.map((conflict) => /* @__PURE__ */ jsxs(Box, {
221
+ flexDirection: "column",
222
+ marginBottom: 1,
223
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
224
+ "Your settings file at ",
225
+ /* @__PURE__ */ jsx(Text, {
226
+ bold: true,
227
+ children: conflict.path
228
+ }),
229
+ " sets:"
230
+ ] }), /* @__PURE__ */ jsx(Box, {
231
+ flexDirection: "column",
232
+ paddingLeft: 2,
233
+ children: conflict.keys.map((key) => /* @__PURE__ */ jsxs(Text, { children: [
234
+ Icons.bullet,
235
+ " ",
236
+ /* @__PURE__ */ jsx(Text, {
237
+ color: "yellow",
238
+ bold: true,
239
+ children: key
240
+ })
241
+ ] }, key))
242
+ })]
243
+ }, conflict.path)), /* @__PURE__ */ jsx(Text, {
244
+ dimColor: true,
245
+ children: "These settings override credentials and prevent the Wizard from reaching the PostHog LLM Gateway. We can back up the file and continue."
246
+ })]
247
+ });
248
+ };
249
+ //#endregion
250
+ //#region src/tui/screens/ManagedSettingsScreen.tsx
251
+ /**
252
+ * ManagedSettingsScreen — Modal for read-only settings conflicts that block
253
+ * the Wizard from reaching the PostHog LLM Gateway: org-managed settings, the
254
+ * user's global `~/.claude` config, and gitignored project-local overrides.
255
+ *
256
+ * Unlike SettingsOverrideScreen, the wizard cannot safely back up or remove
257
+ * these files for the user, so it names the exact file and key and asks the
258
+ * user to fix it and re-run. Managed (root-owned) files need an IT admin; the
259
+ * rest the user can edit themselves.
260
+ */
261
+ function sourceLabel(source) {
262
+ switch (source) {
263
+ case "managed": return "Organization-managed settings";
264
+ case "user": return "Your global Claude Code settings";
265
+ case "project-local": return "Project-local settings";
266
+ default: return source;
267
+ }
268
+ }
269
+ const ManagedSettingsScreen = ({ store }) => {
270
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
271
+ const readOnlyConflicts = store.settingsConflicts?.filter((c) => !c.writable);
272
+ const hasManaged = Boolean(readOnlyConflicts?.some((c) => c.source === "managed"));
273
+ const hasReadOnly = Boolean(readOnlyConflicts && readOnlyConflicts.length > 0);
274
+ useEffect(() => {
275
+ if (hasReadOnly) analytics.wizardCapture("settings conflict shown", {
276
+ kind: "managed",
277
+ has_managed: hasManaged
278
+ });
279
+ }, [hasReadOnly, hasManaged]);
280
+ if (!readOnlyConflicts || readOnlyConflicts.length === 0) return null;
281
+ return /* @__PURE__ */ jsxs(ModalOverlay, {
282
+ borderColor: "red",
283
+ title: `${Icons.warning} Settings conflict`,
284
+ width: 72,
285
+ footer: /* @__PURE__ */ jsx(ConfirmationInput, {
286
+ message: "Fix the file(s) above, then re-run the Wizard.",
287
+ confirmLabel: "",
288
+ cancelLabel: "Exit [Esc]",
289
+ onConfirm: () => store.requestExit(1),
290
+ onCancel: () => store.requestExit(1)
291
+ }),
292
+ children: [
293
+ /* @__PURE__ */ jsx(Text, {
294
+ dimColor: true,
295
+ children: "These Claude Code settings override credentials and prevent the Wizard from reaching the PostHog LLM Gateway."
296
+ }),
297
+ readOnlyConflicts.map((conflict) => /* @__PURE__ */ jsxs(Box, {
298
+ flexDirection: "column",
299
+ marginTop: 1,
300
+ children: [
301
+ /* @__PURE__ */ jsx(Text, {
302
+ bold: true,
303
+ children: sourceLabel(conflict.source)
304
+ }),
305
+ /* @__PURE__ */ jsx(Text, {
306
+ dimColor: true,
307
+ children: conflict.path
308
+ }),
309
+ /* @__PURE__ */ jsx(Box, {
310
+ flexDirection: "column",
311
+ paddingLeft: 2,
312
+ children: conflict.keys.map((key) => /* @__PURE__ */ jsxs(Text, { children: [
313
+ Icons.bullet,
314
+ " ",
315
+ /* @__PURE__ */ jsx(Text, {
316
+ color: "yellow",
317
+ bold: true,
318
+ children: key
319
+ })
320
+ ] }, key))
321
+ })
322
+ ]
323
+ }, conflict.path)),
324
+ /* @__PURE__ */ jsx(Box, {
325
+ marginTop: 1,
326
+ children: /* @__PURE__ */ jsx(Text, {
327
+ dimColor: true,
328
+ children: hasManaged ? "Remove these keys (or run \"claude auth logout\"). Managed files are root-owned — ask your IT administrator." : "Remove these keys, or run \"claude auth logout\", then re-run the Wizard."
329
+ })
330
+ })
331
+ ]
332
+ });
333
+ };
334
+ //#endregion
335
+ //#region src/tui/screens/PortConflictScreen.tsx
336
+ /**
337
+ * PortConflictScreen — Modal when all OAuth port candidates are occupied.
338
+ *
339
+ * Shows every port the wizard tried and asks the user to free them manually.
340
+ */
341
+ const PortConflictScreen = ({ store }) => {
342
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
343
+ if (!store.portConflictProcess) return null;
344
+ return /* @__PURE__ */ jsxs(ModalOverlay, {
345
+ borderColor: "#DC9300",
346
+ title: "OAuth ports in use",
347
+ width: 72,
348
+ footer: /* @__PURE__ */ jsx(ConfirmationInput, {
349
+ message: "Retry after freeing ports?",
350
+ confirmLabel: "Retry [Enter]",
351
+ cancelLabel: "Exit [Esc]",
352
+ onConfirm: () => store.resolvePortConflict(),
353
+ onCancel: () => store.requestExit(1)
354
+ }),
355
+ children: [
356
+ /* @__PURE__ */ jsx(Text, { children: "The wizard needs a local port for OAuth. We tried these ports which are all in use:" }),
357
+ /* @__PURE__ */ jsx(Box, {
358
+ flexDirection: "column",
359
+ marginY: 1,
360
+ paddingLeft: 2,
361
+ gap: 0,
362
+ children: OAUTH_PORTS.map((port) => /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
363
+ dimColor: true,
364
+ children: "Port "
365
+ }), /* @__PURE__ */ jsx(Text, {
366
+ bold: true,
367
+ children: port
368
+ })] }, port))
369
+ }),
370
+ /* @__PURE__ */ jsx(Text, {
371
+ dimColor: true,
372
+ children: "Please free one of these ports and retry."
373
+ })
374
+ ]
375
+ });
376
+ };
377
+ //#endregion
378
+ //#region src/tui/screens/TaskNoticeScreen.tsx
379
+ /**
380
+ * TaskNoticeScreen — Modal shown before an optional step runs.
381
+ *
382
+ * Some steps stop to ask the user for something. This tells them so before the
383
+ * step starts, and lets them decline it. The copy comes from the program that
384
+ * owns the step; this screen only renders what it is given.
385
+ */
386
+ const TaskNoticeScreen = ({ store }) => {
387
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
388
+ const notice = store.session.taskNotice;
389
+ if (!notice) return null;
390
+ return /* @__PURE__ */ jsxs(ModalOverlay, {
391
+ borderColor: Colors.primary,
392
+ title: notice.title,
393
+ width: 76,
394
+ footer: /* @__PURE__ */ jsx(ConfirmationInput, {
395
+ message: notice.prompt,
396
+ confirmLabel: notice.confirmLabel,
397
+ cancelLabel: notice.cancelLabel,
398
+ onConfirm: () => store.resolveTaskNotice(true),
399
+ onCancel: () => store.resolveTaskNotice(false)
400
+ }),
401
+ children: [
402
+ /* @__PURE__ */ jsx(Box, {
403
+ flexDirection: "column",
404
+ gap: 1,
405
+ children: notice.body.map((paragraph) => /* @__PURE__ */ jsx(Text, { children: paragraph }, paragraph))
406
+ }),
407
+ notice.items && notice.items.length > 0 && /* @__PURE__ */ jsx(Box, {
408
+ marginTop: 1,
409
+ paddingLeft: 2,
410
+ children: /* @__PURE__ */ jsx(Text, {
411
+ bold: true,
412
+ children: notice.items.join(", ")
413
+ })
414
+ }),
415
+ notice.docsUrl && /* @__PURE__ */ jsx(Box, {
416
+ marginTop: 1,
417
+ children: /* @__PURE__ */ jsxs(Text, {
418
+ dimColor: true,
419
+ children: [
420
+ notice.docsLabel ?? "Learn more",
421
+ ": ",
422
+ notice.docsUrl
423
+ ]
424
+ })
425
+ })
426
+ ]
427
+ });
428
+ };
429
+ //#endregion
430
+ //#region src/tui/screens/ManualAuthCodeScreen.tsx
431
+ /**
432
+ * ManualAuthCodeScreen — Paste an OAuth authorization code by hand.
433
+ *
434
+ * Fallback for headless/remote shells where the browser can't reach the
435
+ * wizard's local callback server. Shows the direct authorize URL (the localhost
436
+ * one is unreachable from another machine), then the user pastes either the
437
+ * full callback URL the browser was redirected to
438
+ * (`http://localhost:8239/callback?code=...`) or just the code. On submit we
439
+ * extract the code and resolve the in-flight OAuth flow; bad input shows inline
440
+ * feedback without leaving the screen.
441
+ *
442
+ * Opened from AuthScreen via a keypress; Esc dismisses it.
443
+ */
444
+ const ManualAuthCodeScreen = ({ store }) => {
445
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
446
+ const { authorizeUrl } = store;
447
+ const [error, setError] = useState(null);
448
+ useInput((_input, key) => {
449
+ if (key.escape) store.dismissManualAuthCode();
450
+ });
451
+ const handleSubmit = (value) => {
452
+ const code = extractOAuthCode(value);
453
+ if (!code) {
454
+ setError("Couldn't find a code in that input. Paste the full callback URL or just the code.");
455
+ return;
456
+ }
457
+ store.submitManualAuthCode(code);
458
+ };
459
+ return /* @__PURE__ */ jsxs(Box, {
460
+ flexDirection: "column",
461
+ flexGrow: 1,
462
+ children: [
463
+ /* @__PURE__ */ jsx(Box, {
464
+ marginBottom: 1,
465
+ children: /* @__PURE__ */ jsxs(Text, {
466
+ bold: true,
467
+ color: Colors.accent,
468
+ children: [Icons.diamond, " Paste authorization code"]
469
+ })
470
+ }),
471
+ authorizeUrl && /* @__PURE__ */ jsxs(Box, {
472
+ flexDirection: "column",
473
+ marginBottom: 1,
474
+ children: [/* @__PURE__ */ jsx(Text, {
475
+ dimColor: true,
476
+ children: "On a remote/headless machine the local login link won't open. Open this URL in a browser on any machine to authorize:"
477
+ }), /* @__PURE__ */ jsx(Box, {
478
+ flexShrink: 0,
479
+ marginTop: 1,
480
+ children: /* @__PURE__ */ jsx(Text, {
481
+ color: "cyan",
482
+ children: authorizeUrl
483
+ })
484
+ })]
485
+ }),
486
+ /* @__PURE__ */ jsx(Text, { children: "After authorizing, paste the callback URL it lands on — or just the code from it — here:" }),
487
+ /* @__PURE__ */ jsx(Box, {
488
+ marginTop: 1,
489
+ width: "100%",
490
+ children: /* @__PURE__ */ jsx(TextInput, {
491
+ placeholder: "http://localhost:8239/callback?code=… or the code",
492
+ onSubmit: handleSubmit
493
+ })
494
+ }),
495
+ error && /* @__PURE__ */ jsx(Box, {
496
+ marginTop: 1,
497
+ children: /* @__PURE__ */ jsx(Text, {
498
+ color: "yellow",
499
+ children: error
500
+ })
501
+ }),
502
+ /* @__PURE__ */ jsx(Box, {
503
+ marginTop: 1,
504
+ children: /* @__PURE__ */ jsxs(Text, { children: [
505
+ /* @__PURE__ */ jsx(Text, {
506
+ color: Colors.accent,
507
+ children: "ENTER"
508
+ }),
509
+ /* @__PURE__ */ jsx(Text, {
510
+ dimColor: true,
511
+ children: " submit"
512
+ }),
513
+ /* @__PURE__ */ jsx(Text, {
514
+ dimColor: true,
515
+ children: " · "
516
+ }),
517
+ /* @__PURE__ */ jsx(Text, {
518
+ color: Colors.accent,
519
+ children: "ESC"
520
+ }),
521
+ /* @__PURE__ */ jsx(Text, {
522
+ dimColor: true,
523
+ children: " cancel"
524
+ })
525
+ ] })
526
+ })
527
+ ]
528
+ });
529
+ };
530
+ //#endregion
531
+ //#region src/tui/screens/SetupScreen.tsx
532
+ /**
533
+ * SetupScreen — Generic framework disambiguation.
534
+ *
535
+ * Iterates unresolved setup questions from the FrameworkConfig
536
+ * and renders a PickerMenu for each. If all questions are auto-resolved,
537
+ * this screen is skipped entirely (the router skips it via its show() predicate).
538
+ */
539
+ const SetupScreen = ({ store }) => {
540
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
541
+ const config = store.session.frameworkConfig;
542
+ const questions = config?.metadata.setup?.questions ?? [];
543
+ const [currentIndex, setCurrentIndex] = useState(0);
544
+ const [resolving, setResolving] = useState(true);
545
+ useEffect(() => {
546
+ (async () => {
547
+ for (const q of questions) {
548
+ if (q.key in store.session.frameworkContext) continue;
549
+ try {
550
+ const detected = await q.detect({ installDir: store.session.installDir });
551
+ if (detected !== null) store.setFrameworkContext(q.key, detected);
552
+ } catch {}
553
+ }
554
+ setResolving(false);
555
+ })();
556
+ }, []);
557
+ if (resolving) return /* @__PURE__ */ jsx(Box, {
558
+ flexDirection: "column",
559
+ flexGrow: 1,
560
+ children: /* @__PURE__ */ jsx(Text, {
561
+ dimColor: true,
562
+ children: "Detecting project configuration..."
563
+ })
564
+ });
565
+ const unresolved = questions.filter((q) => !(q.key in store.session.frameworkContext));
566
+ if (unresolved.length === 0) return null;
567
+ const question = unresolved[currentIndex] ?? unresolved[0];
568
+ if (!question) return null;
569
+ return /* @__PURE__ */ jsxs(Box, {
570
+ flexDirection: "column",
571
+ flexGrow: 1,
572
+ children: [/* @__PURE__ */ jsxs(Box, {
573
+ flexDirection: "column",
574
+ marginBottom: 1,
575
+ children: [/* @__PURE__ */ jsx(Text, {
576
+ bold: true,
577
+ color: Colors.accent,
578
+ children: "Project Setup"
579
+ }), config && /* @__PURE__ */ jsxs(Text, {
580
+ dimColor: true,
581
+ children: [
582
+ "Configuring ",
583
+ config.metadata.name,
584
+ " integration"
585
+ ]
586
+ })]
587
+ }), /* @__PURE__ */ jsx(PickerMenu, {
588
+ message: question.message,
589
+ options: question.options.map((o) => ({
590
+ label: o.label,
591
+ value: o.value,
592
+ hint: o.hint
593
+ })),
594
+ onSelect: (value) => {
595
+ const selected = Array.isArray(value) ? value[0] : value;
596
+ store.setFrameworkContext(question.key, selected);
597
+ if (unresolved.filter((q) => q.key !== question.key && !(q.key in store.session.frameworkContext)).length > 0) setCurrentIndex((i) => i + 1);
598
+ }
599
+ })]
600
+ });
601
+ };
602
+ //#endregion
603
+ //#region src/shared/utils/clipboard.ts
604
+ /**
605
+ * Best-effort platform shell-outs for making a URL usable in any terminal —
606
+ * no third-party dependency.
607
+ *
608
+ * Two helpers back the `wizard_ask` link overlay, for terminals that can't
609
+ * linkify a wrapped URL (e.g. macOS Terminal.app, which lacks OSC 8 hyperlink
610
+ * support):
611
+ *
612
+ * - `copyToClipboard` — pipe the URL to the platform clipboard binary so the
613
+ * user can paste it into a browser.
614
+ * - `openInBrowser` — hand the URL to the platform opener so it launches
615
+ * directly, no copy/paste needed.
616
+ *
617
+ * Both shell out with `spawn` and never a shell, and pass the URL as an argv
618
+ * (clipboard via stdin) — so there's no shell-injection surface. Neither throws:
619
+ * they return false when no command is reachable (CI, headless, or the binary
620
+ * isn't installed).
621
+ */
622
+ /** Platform clipboard commands, in preference order. */
623
+ function clipboardCommands() {
624
+ if (process.platform === "darwin") return [{
625
+ cmd: "pbcopy",
626
+ args: []
627
+ }];
628
+ if (process.platform === "win32") return [{
629
+ cmd: "clip",
630
+ args: []
631
+ }];
632
+ return [
633
+ {
634
+ cmd: "wl-copy",
635
+ args: []
636
+ },
637
+ {
638
+ cmd: "xclip",
639
+ args: ["-selection", "clipboard"]
640
+ },
641
+ {
642
+ cmd: "xsel",
643
+ args: ["--clipboard", "--input"]
644
+ }
645
+ ];
646
+ }
647
+ function writeWith({ cmd, args }, text) {
648
+ return new Promise((resolve) => {
649
+ try {
650
+ const child = spawn(cmd, args);
651
+ child.on("error", () => resolve(false));
652
+ child.stdin.on("error", () => resolve(false));
653
+ child.on("close", (code) => resolve(code === 0));
654
+ child.stdin.end(text);
655
+ } catch {
656
+ resolve(false);
657
+ }
658
+ });
659
+ }
660
+ async function copyToClipboard(text) {
661
+ for (const command of clipboardCommands()) if (await writeWith(command, text)) return true;
662
+ logToFile("[clipboard] no working clipboard command found");
663
+ return false;
664
+ }
665
+ /**
666
+ * Platform browser-open commands, in preference order. The URL is one argv
667
+ * element — never concatenated into a shell string — so a `?kind=…` query or
668
+ * any other character can't escape into the command line.
669
+ *
670
+ * `win32` has no shell-free single binary for this: `start` is a `cmd` builtin,
671
+ * so we invoke `cmd /c start "" <url>` (the empty `""` is `start`'s window-title
672
+ * argument, which would otherwise swallow the URL). The URL is still a discrete
673
+ * argv element, not interpolated into the command string.
674
+ *
675
+ * Takes `platform` as an argument (defaulting to the host) so the mapping is a
676
+ * pure function the tests can exercise across platforms.
677
+ */
678
+ function browserOpenCommands(url, platform = process.platform) {
679
+ if (platform === "darwin") return [{
680
+ cmd: "open",
681
+ args: [url]
682
+ }];
683
+ if (platform === "win32") return [{
684
+ cmd: "cmd",
685
+ args: [
686
+ "/c",
687
+ "start",
688
+ "",
689
+ url
690
+ ]
691
+ }];
692
+ return [{
693
+ cmd: "xdg-open",
694
+ args: [url]
695
+ }, {
696
+ cmd: "x-www-browser",
697
+ args: [url]
698
+ }];
699
+ }
700
+ function spawnOpener({ cmd, args }) {
701
+ return new Promise((resolve) => {
702
+ try {
703
+ const child = spawn(cmd, args, { stdio: "ignore" });
704
+ child.on("error", () => resolve(false));
705
+ child.on("close", (code) => resolve(code === 0));
706
+ } catch {
707
+ resolve(false);
708
+ }
709
+ });
710
+ }
711
+ async function openInBrowser(url) {
712
+ for (const command of browserOpenCommands(url)) if (await spawnOpener(command)) return true;
713
+ logToFile("[clipboard] no working browser-open command found");
714
+ return false;
715
+ }
716
+ //#endregion
717
+ //#region src/tui/screens/AuthScreen.tsx
718
+ /**
719
+ * AuthScreen — Shown while waiting for OAuth authentication.
720
+ *
721
+ * Displays framework detection, a compressed privacy summary, a waiting
722
+ * spinner, and the login URL when available. [I] opens the full
723
+ * PrivacyPanel as an overlay. [P] (when loginUrl is set) lets the user
724
+ * paste the callback URL by hand. [C] copies the login URL to the
725
+ * clipboard — the exact string, so it can't be chopped by a soft-wrap.
726
+ *
727
+ * The router resolves past this screen once session.credentials is set.
728
+ */
729
+ const AuthScreen = ({ store }) => {
730
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
731
+ const [showPrivacy, setShowPrivacy] = useState(false);
732
+ const [copyStatus, setCopyStatus] = useState("idle");
733
+ const [columns] = useStdoutDimensions();
734
+ const { session } = store;
735
+ const loginUrl = store.loginUrl;
736
+ const canPasteCode = Boolean(loginUrl);
737
+ const availableWidth = Math.min(columns, 120) - 2;
738
+ const urlFits = loginUrl ? loginUrl.length <= availableWidth : true;
739
+ const bindings = [];
740
+ if (!showPrivacy) {
741
+ if (canPasteCode && loginUrl) {
742
+ bindings.push({
743
+ match: ["p", "P"],
744
+ label: "P",
745
+ action: "paste auth code",
746
+ handler: () => store.showManualAuthCode()
747
+ });
748
+ bindings.push({
749
+ match: ["c", "C"],
750
+ label: "C",
751
+ action: "copy link",
752
+ handler: () => {
753
+ copyToClipboard(loginUrl).then((ok) => setCopyStatus(ok ? "copied" : "failed"));
754
+ }
755
+ });
756
+ }
757
+ bindings.push({
758
+ match: ["i", "I"],
759
+ label: "I",
760
+ action: PRIVACY_PANEL_LABEL.toLowerCase(),
761
+ handler: () => setShowPrivacy(true)
762
+ });
763
+ }
764
+ useKeyBindings("auth", bindings);
765
+ if (showPrivacy) return /* @__PURE__ */ jsx(IntroScreenLayout, {
766
+ installDir: session.installDir,
767
+ title: PRIVACY_PANEL_LABEL,
768
+ showSubtitle: false,
769
+ showDetection: false,
770
+ body: /* @__PURE__ */ jsx(PrivacyPanel, {}),
771
+ showPrivacy: false,
772
+ menuOptions: [{
773
+ label: "Back",
774
+ value: "back"
775
+ }],
776
+ menuAlign: "left",
777
+ onSelect: () => setShowPrivacy(false)
778
+ });
779
+ const config = session.frameworkConfig;
780
+ const frameworkLabel = session.detectedFrameworkLabel ?? config?.metadata.name;
781
+ return /* @__PURE__ */ jsxs(Box, {
782
+ flexDirection: "column",
783
+ flexGrow: 1,
784
+ children: [
785
+ /* @__PURE__ */ jsxs(Box, {
786
+ flexDirection: "column",
787
+ marginBottom: 1,
788
+ children: [
789
+ /* @__PURE__ */ jsx(Text, {
790
+ bold: true,
791
+ color: Colors.accent,
792
+ children: "PostHog Setup Wizard"
793
+ }),
794
+ frameworkLabel && /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
795
+ color: "green",
796
+ children: ["✔", " "]
797
+ }), /* @__PURE__ */ jsxs(Text, { children: ["Framework: ", frameworkLabel] })] }),
798
+ config?.metadata.beta && /* @__PURE__ */ jsxs(Text, {
799
+ color: "yellow",
800
+ children: [
801
+ "[BETA] The ",
802
+ config.metadata.name,
803
+ " wizard is in beta. Questions or feedback? Email wizard@posthog.com"
804
+ ]
805
+ }),
806
+ config?.metadata.preRunNotice && /* @__PURE__ */ jsx(Text, {
807
+ color: "yellow",
808
+ children: config.metadata.preRunNotice
809
+ })
810
+ ]
811
+ }),
812
+ /* @__PURE__ */ jsxs(Box, {
813
+ flexDirection: "column",
814
+ marginBottom: 1,
815
+ children: [
816
+ /* @__PURE__ */ jsx(Text, {
817
+ bold: true,
818
+ dimColor: true,
819
+ children: PRIVACY_PANEL_LABEL
820
+ }),
821
+ /* @__PURE__ */ jsxs(Text, {
822
+ dimColor: true,
823
+ children: ["•", " Source files are read by Claude for AI context"]
824
+ }),
825
+ /* @__PURE__ */ jsxs(Text, {
826
+ dimColor: true,
827
+ children: ["•", " .env* and secrets stay on your machine"]
828
+ }),
829
+ /* @__PURE__ */ jsxs(Text, {
830
+ dimColor: true,
831
+ children: [
832
+ "•",
833
+ " Press ",
834
+ /* @__PURE__ */ jsx(Text, {
835
+ color: Colors.accent,
836
+ children: "[I]"
837
+ }),
838
+ " for full privacy & usage info"
839
+ ]
840
+ })
841
+ ]
842
+ }),
843
+ /* @__PURE__ */ jsx(LoadingBox, { message: "Waiting for authentication..." }),
844
+ loginUrl && /* @__PURE__ */ jsxs(Box, {
845
+ marginTop: 1,
846
+ marginBottom: 1,
847
+ flexDirection: "column",
848
+ children: [
849
+ urlFits ? /* @__PURE__ */ jsxs(Text, { children: [
850
+ /* @__PURE__ */ jsx(Text, {
851
+ dimColor: true,
852
+ children: "If the browser didn't open, copy and paste this URL:"
853
+ }),
854
+ "\n\n",
855
+ /* @__PURE__ */ jsx(Text, {
856
+ color: "cyan",
857
+ children: loginUrl
858
+ })
859
+ ] }) : /* @__PURE__ */ jsx(Text, {
860
+ color: "yellow",
861
+ children: "[This terminal is too small to show the full link for copying, resize your terminal]"
862
+ }),
863
+ /* @__PURE__ */ jsx(Box, {
864
+ marginTop: 1,
865
+ children: /* @__PURE__ */ jsxs(Text, {
866
+ dimColor: true,
867
+ children: [
868
+ "Press ",
869
+ /* @__PURE__ */ jsx(Text, {
870
+ color: Colors.accent,
871
+ children: "[C]"
872
+ }),
873
+ " to copy the link · on a remote machine or devbox? Press",
874
+ " ",
875
+ /* @__PURE__ */ jsx(Text, {
876
+ color: Colors.accent,
877
+ children: "[P]"
878
+ }),
879
+ " to paste the callback URL."
880
+ ]
881
+ })
882
+ }),
883
+ copyStatus === "copied" && /* @__PURE__ */ jsx(Box, {
884
+ marginTop: 1,
885
+ children: /* @__PURE__ */ jsxs(Text, {
886
+ color: Colors.success,
887
+ children: [Icons.check, " Link copied to clipboard."]
888
+ })
889
+ }),
890
+ copyStatus === "failed" && /* @__PURE__ */ jsx(Box, {
891
+ marginTop: 1,
892
+ children: /* @__PURE__ */ jsxs(Text, {
893
+ color: "yellow",
894
+ children: [
895
+ "Couldn't reach a clipboard on this machine — press",
896
+ " ",
897
+ /* @__PURE__ */ jsx(Text, {
898
+ color: Colors.accent,
899
+ children: "[P]"
900
+ }),
901
+ " to paste the callback URL instead."
902
+ ]
903
+ })
904
+ })
905
+ ]
906
+ })
907
+ ]
908
+ });
909
+ };
910
+ //#endregion
911
+ //#region src/tui/screens/RunScreen.tsx
912
+ /**
913
+ * RunScreen — Default observational view of the agent run.
914
+ *
915
+ * Tabs: Status (LearnCard + ProgressList), Event plan (when present),
916
+ * Tail logs, HN. Programs that need a different tab list ship their own
917
+ * screen component (see audit/AuditRunScreen.tsx).
918
+ */
919
+ const RunScreen = ({ store }) => {
920
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
921
+ const [columns] = useStdoutDimensions();
922
+ const progressItems = store.tasks.map((t) => ({
923
+ label: t.label,
924
+ activeForm: t.activeForm,
925
+ status: t.status
926
+ }));
927
+ const statuses = store.statusMessages.length > 0 ? store.statusMessages : void 0;
928
+ const activeProgram = store.router.activeProgram;
929
+ const learnBlocks = useMemo(() => {
930
+ return (flowOwner(activeProgram).deck ?? getContentBlocks)(store);
931
+ }, [store, activeProgram]);
932
+ const programTips = flowOwner(activeProgram).tips?.(store);
933
+ const leftPane = store.learnCardComplete ? /* @__PURE__ */ jsx(TipsCard, {
934
+ store,
935
+ tips: programTips
936
+ }) : /* @__PURE__ */ jsx(LearnCard, {
937
+ store,
938
+ blocks: learnBlocks,
939
+ onComplete: () => store.setLearnCardComplete()
940
+ });
941
+ const progressList = /* @__PURE__ */ jsx(ProgressList, {
942
+ items: progressItems,
943
+ title: "Tasks"
944
+ });
945
+ return /* @__PURE__ */ jsx(TabContainer, {
946
+ tabs: [
947
+ {
948
+ id: "status",
949
+ label: "Status",
950
+ component: columns < 80 ? /* @__PURE__ */ jsx(Box, {
951
+ flexDirection: "column",
952
+ flexGrow: 1,
953
+ children: progressList
954
+ }) : /* @__PURE__ */ jsx(SplitView, {
955
+ left: leftPane,
956
+ right: progressList
957
+ })
958
+ },
959
+ ...store.eventPlan.length > 0 ? [{
960
+ id: "events",
961
+ label: "Event plan",
962
+ component: /* @__PURE__ */ jsx(EventPlanViewer, { events: store.eventPlan })
963
+ }] : [],
964
+ {
965
+ id: "logs",
966
+ label: "Tail logs",
967
+ component: /* @__PURE__ */ jsx(LogViewer, { filePath: getLogFilePath() })
968
+ },
969
+ {
970
+ id: "visualizer",
971
+ label: "Visualizer",
972
+ component: /* @__PURE__ */ jsx(VisualizerTab, { store })
973
+ },
974
+ {
975
+ id: "hn",
976
+ label: "HN",
977
+ component: /* @__PURE__ */ jsx(HNViewer, {})
978
+ }
979
+ ],
980
+ statusMessage: statuses,
981
+ expandableStatus: true,
982
+ store
983
+ });
984
+ };
985
+ //#endregion
986
+ //#region src/tui/screens/KeepSkillsScreen.tsx
987
+ /**
988
+ * KeepSkillsScreen — Ask whether to keep installed skills in .claude/skills/.
989
+ *
990
+ * Shown after the outro summary so users see the agent's output first,
991
+ * then decide whether to keep the skills that powered it.
992
+ *
993
+ * When done, calls store.setSkillsComplete() and exits the process.
994
+ */
995
+ const WIZARD_MARKER = ".posthog-wizard";
996
+ const KeepSkillsScreen = ({ store }) => {
997
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
998
+ const [phase, setPhase] = useState("loading");
999
+ const [skills, setSkills] = useState([]);
1000
+ const skillsDir = join$1(store.session.installDir, ".claude", "skills");
1001
+ useEffect(() => {
1002
+ (async () => {
1003
+ try {
1004
+ const dirs = (await readdir(skillsDir, { withFileTypes: true })).filter((e) => e.isDirectory());
1005
+ const result = [];
1006
+ for (const dir of dirs) {
1007
+ try {
1008
+ await access(join$1(skillsDir, dir.name, WIZARD_MARKER));
1009
+ } catch {
1010
+ continue;
1011
+ }
1012
+ const children = (await readdir(join$1(skillsDir, dir.name))).filter((c) => c !== WIZARD_MARKER);
1013
+ result.push({
1014
+ name: dir.name,
1015
+ children
1016
+ });
1017
+ }
1018
+ if (result.length === 0) {
1019
+ store.setSkillsComplete(true);
1020
+ exit();
1021
+ return;
1022
+ }
1023
+ setSkills(result);
1024
+ setPhase("ask");
1025
+ } catch {
1026
+ store.setSkillsComplete(true);
1027
+ exit();
1028
+ }
1029
+ })();
1030
+ }, []);
1031
+ const exit = () => {
1032
+ if (!store.mintHandoff) store.requestExit(0);
1033
+ };
1034
+ const handleKeep = () => {
1035
+ store.setSkillsComplete(true);
1036
+ exit();
1037
+ };
1038
+ const handleRemove = async () => {
1039
+ setPhase("removing");
1040
+ for (const skill of skills) try {
1041
+ await rm(join$1(skillsDir, skill.name), {
1042
+ recursive: true,
1043
+ force: true
1044
+ });
1045
+ } catch {}
1046
+ try {
1047
+ if ((await readdir(skillsDir)).length === 0) await rm(skillsDir, {
1048
+ recursive: true,
1049
+ force: true
1050
+ });
1051
+ } catch {}
1052
+ setPhase("done");
1053
+ setTimeout(() => {
1054
+ store.setSkillsComplete(false);
1055
+ exit();
1056
+ }, 600);
1057
+ };
1058
+ return /* @__PURE__ */ jsxs(Box, {
1059
+ flexDirection: "column",
1060
+ flexGrow: 1,
1061
+ children: [/* @__PURE__ */ jsx(Text, {
1062
+ bold: true,
1063
+ color: Colors.accent,
1064
+ children: "Keep the skills?"
1065
+ }), /* @__PURE__ */ jsxs(Box, {
1066
+ marginTop: 1,
1067
+ flexDirection: "column",
1068
+ children: [
1069
+ phase === "loading" && /* @__PURE__ */ jsx(Text, {
1070
+ dimColor: true,
1071
+ children: "Checking installed skills..."
1072
+ }),
1073
+ phase === "ask" && /* @__PURE__ */ jsxs(Fragment$1, { children: [
1074
+ /* @__PURE__ */ jsx(Text, {
1075
+ dimColor: true,
1076
+ children: "The wizard installed open-source skills that help AI coding agents integrate PostHog into your project:"
1077
+ }),
1078
+ /* @__PURE__ */ jsxs(Box, {
1079
+ marginTop: 1,
1080
+ flexDirection: "column",
1081
+ marginLeft: 2,
1082
+ children: [
1083
+ /* @__PURE__ */ jsx(Text, {
1084
+ dimColor: true,
1085
+ children: ".claude/"
1086
+ }),
1087
+ /* @__PURE__ */ jsx(Text, {
1088
+ dimColor: true,
1089
+ children: " skills/"
1090
+ }),
1091
+ skills.map((skill) => /* @__PURE__ */ jsxs(Box, {
1092
+ flexDirection: "column",
1093
+ children: [/* @__PURE__ */ jsxs(Text, {
1094
+ dimColor: true,
1095
+ children: [
1096
+ " ",
1097
+ skill.name,
1098
+ "/"
1099
+ ]
1100
+ }), skill.children.map((child) => /* @__PURE__ */ jsxs(Text, {
1101
+ dimColor: true,
1102
+ children: [" ", child]
1103
+ }, child))]
1104
+ }, skill.name))
1105
+ ]
1106
+ }),
1107
+ /* @__PURE__ */ jsx(Box, {
1108
+ marginTop: 1,
1109
+ children: /* @__PURE__ */ jsxs(Text, {
1110
+ dimColor: true,
1111
+ children: ["Source: ", /* @__PURE__ */ jsx(Text, {
1112
+ color: "cyan",
1113
+ children: "https://github.com/PostHog/context-mill"
1114
+ })]
1115
+ })
1116
+ }),
1117
+ /* @__PURE__ */ jsx(Box, {
1118
+ marginTop: 1,
1119
+ children: /* @__PURE__ */ jsx(ConfirmationInput, {
1120
+ message: "Keep the installed skills?",
1121
+ confirmLabel: "Keep [Enter]",
1122
+ cancelLabel: "Remove [Esc]",
1123
+ onConfirm: handleKeep,
1124
+ onCancel: () => void handleRemove()
1125
+ })
1126
+ })
1127
+ ] }),
1128
+ phase === "removing" && /* @__PURE__ */ jsx(Text, {
1129
+ dimColor: true,
1130
+ children: "Removing skills..."
1131
+ }),
1132
+ phase === "done" && /* @__PURE__ */ jsx(Text, {
1133
+ dimColor: true,
1134
+ children: "Skills removed."
1135
+ })
1136
+ ]
1137
+ })]
1138
+ });
1139
+ };
1140
+ //#endregion
1141
+ //#region src/tui/screens/MintFailureScreen.tsx
1142
+ /** Two blocks of the PostHog flag, left of the headline. */
1143
+ const Mark = () => /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
1144
+ color: "#1D4AFF",
1145
+ children: "█"
1146
+ }), /* @__PURE__ */ jsx(Text, {
1147
+ color: Colors.accent,
1148
+ children: "█"
1149
+ })] });
1150
+ function MintFailureScreen({ store, services }) {
1151
+ const [columns] = useStdoutDimensions();
1152
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
1153
+ const busy = useRef(false);
1154
+ const [working, setWorking] = useState(null);
1155
+ const [error, setError] = useState(null);
1156
+ const [report, setReport] = useState(false);
1157
+ const [retry, setRetry] = useState("save");
1158
+ const { spellbook } = store;
1159
+ const select = async (action) => {
1160
+ if (busy.current) return;
1161
+ if (action === "exit" || action === "continue") return store.setMintHandoff(action);
1162
+ if (action === "report") return setReport(true);
1163
+ const task = action === "retry" ? retry : action;
1164
+ setRetry(task);
1165
+ busy.current = true;
1166
+ setError(null);
1167
+ setWorking("Saving skill...");
1168
+ try {
1169
+ const saved = store.spellbook ?? await services.leaveSpellbook();
1170
+ store.setSpellbook(saved);
1171
+ if (task !== "save") {
1172
+ setWorking(`Opening ${task === "claude" ? "Claude Code" : "Codex"}...`);
1173
+ await services.openAgent(task, saved.path);
1174
+ store.setMintHandoff("continue");
1175
+ }
1176
+ } catch (err) {
1177
+ setError(store.spellbook ? err instanceof Error ? err.message : "Could not open your agent." : "Could not save the skill. Check this folder is writable and try again.");
1178
+ } finally {
1179
+ busy.current = false;
1180
+ setWorking(null);
1181
+ }
1182
+ };
1183
+ const options = error ? [
1184
+ {
1185
+ label: "Try again",
1186
+ value: "retry"
1187
+ },
1188
+ ...spellbook ? [{
1189
+ label: "Continue setup",
1190
+ value: "continue"
1191
+ }] : [],
1192
+ {
1193
+ label: "Exit",
1194
+ value: "exit"
1195
+ }
1196
+ ] : spellbook ? [{
1197
+ label: "Continue",
1198
+ value: "continue"
1199
+ }, {
1200
+ label: "Exit",
1201
+ value: "exit"
1202
+ }] : report ? [{
1203
+ label: "Save skill",
1204
+ value: "save"
1205
+ }, {
1206
+ label: "Exit",
1207
+ value: "exit"
1208
+ }] : [
1209
+ {
1210
+ label: "Save skill",
1211
+ value: "save"
1212
+ },
1213
+ {
1214
+ label: "Open in Claude Code",
1215
+ value: "claude"
1216
+ },
1217
+ {
1218
+ label: "Open in Codex",
1219
+ value: "codex"
1220
+ },
1221
+ {
1222
+ label: "Report this issue",
1223
+ value: "report"
1224
+ },
1225
+ {
1226
+ label: "Exit",
1227
+ value: "exit"
1228
+ }
1229
+ ];
1230
+ return /* @__PURE__ */ jsx(Box, {
1231
+ flexDirection: "column",
1232
+ flexGrow: 1,
1233
+ alignItems: "center",
1234
+ justifyContent: "center",
1235
+ children: /* @__PURE__ */ jsxs(Box, {
1236
+ width: Math.max(1, Math.min(78, columns - 4)),
1237
+ flexDirection: "column",
1238
+ alignItems: "center",
1239
+ children: [
1240
+ /* @__PURE__ */ jsxs(Text, {
1241
+ bold: true,
1242
+ children: [
1243
+ /* @__PURE__ */ jsx(Mark, {}),
1244
+ " ",
1245
+ spellbook ? "Skill saved" : MINT_FAILURE_MESSAGE,
1246
+ " 🦔"
1247
+ ]
1248
+ }),
1249
+ /* @__PURE__ */ jsx(Box, {
1250
+ marginTop: 1,
1251
+ children: /* @__PURE__ */ jsx(Text, { children: spellbook ? spellbook.skillsIncluded ? "Ask your agent to read this file and complete the task." : "The skill could not be downloaded. This file has instructions and links for your agent." : report ? MINT_FAILURE_CONTACT : MINT_FAILURE_BODY })
1252
+ }),
1253
+ report && !spellbook && /* @__PURE__ */ jsx(Box, {
1254
+ marginTop: 1,
1255
+ children: /* @__PURE__ */ jsx(Text, {
1256
+ color: "cyan",
1257
+ wrap: "wrap",
1258
+ children: services.logPath
1259
+ })
1260
+ }),
1261
+ spellbook && /* @__PURE__ */ jsx(Box, {
1262
+ marginTop: 1,
1263
+ children: /* @__PURE__ */ jsx(Text, {
1264
+ color: "cyan",
1265
+ wrap: "wrap",
1266
+ children: spellbook.path
1267
+ })
1268
+ }),
1269
+ error && /* @__PURE__ */ jsx(Box, {
1270
+ marginTop: 1,
1271
+ children: /* @__PURE__ */ jsx(Text, {
1272
+ color: Colors.error,
1273
+ children: error
1274
+ })
1275
+ }),
1276
+ /* @__PURE__ */ jsx(Box, {
1277
+ marginTop: 1,
1278
+ flexDirection: "column",
1279
+ children: working ? /* @__PURE__ */ jsx(Text, { children: working }) : /* @__PURE__ */ jsx(PickerMenu, {
1280
+ options,
1281
+ onSelect: (action) => void select(action)
1282
+ }, [
1283
+ report,
1284
+ Boolean(spellbook),
1285
+ Boolean(error)
1286
+ ].join("-"))
1287
+ })
1288
+ ]
1289
+ })
1290
+ });
1291
+ }
1292
+ //#endregion
1293
+ //#region src/tui/services/coding-agent-launcher.ts
1294
+ const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
1295
+ const batchQuote = (value) => `"${value.replace(/%/g, "%%")}"`;
1296
+ async function executable(name) {
1297
+ const extensions = process.platform === "win32" ? [
1298
+ ".exe",
1299
+ ".cmd",
1300
+ ".bat"
1301
+ ] : [""];
1302
+ for (const directory of (process.env.PATH ?? "").split(path$1.delimiter)) {
1303
+ if (!path$1.isAbsolute(directory)) continue;
1304
+ for (const extension of extensions) {
1305
+ const candidate = path$1.join(directory, name + extension);
1306
+ try {
1307
+ await access(candidate, constants.X_OK);
1308
+ return candidate;
1309
+ } catch {
1310
+ continue;
1311
+ }
1312
+ }
1313
+ }
1314
+ throw new Error(`${name === "claude" ? "Claude Code" : name === "codex" ? "Codex" : name} is not installed or could not be found.`);
1315
+ }
1316
+ function launch(command, args, cwd, verbatim = false) {
1317
+ return new Promise((resolve, reject) => {
1318
+ const child = spawn(command, args, {
1319
+ cwd,
1320
+ stdio: "ignore",
1321
+ detached: true,
1322
+ shell: false,
1323
+ windowsVerbatimArguments: verbatim
1324
+ });
1325
+ const timer = setTimeout(() => {
1326
+ child.unref();
1327
+ resolve();
1328
+ }, 500);
1329
+ child.once("error", (error) => {
1330
+ clearTimeout(timer);
1331
+ reject(error);
1332
+ });
1333
+ child.once("exit", (code) => {
1334
+ clearTimeout(timer);
1335
+ if (code === 0) resolve();
1336
+ else reject(/* @__PURE__ */ new Error("Could not open a terminal for your agent."));
1337
+ });
1338
+ });
1339
+ }
1340
+ async function openCodingAgent(agent, directory, spellbookPath) {
1341
+ const cwd = path$1.resolve(directory);
1342
+ const command = await executable(agent);
1343
+ const prompt = `Read <${path$1.resolve(spellbookPath)}> and complete the task described in the Wizard's spell books. Inspect what has already been done before making changes.`;
1344
+ if (process.platform === "linux") {
1345
+ await launch(await executable("x-terminal-emulator"), [
1346
+ "-e",
1347
+ command,
1348
+ prompt
1349
+ ], cwd);
1350
+ return;
1351
+ }
1352
+ const mac = process.platform === "darwin";
1353
+ if (!mac && process.platform !== "win32") throw new Error("Opening an agent is not supported on this system. Use the saved skill path to continue.");
1354
+ const folder = await mkdtemp(path$1.join(tmpdir(), "wizard-handoff-"));
1355
+ const script = path$1.join(folder, mac ? "open-agent.command" : "open-agent.cmd");
1356
+ try {
1357
+ await writeFile(script, (mac ? [
1358
+ "#!/bin/sh",
1359
+ `rm -f ${quote(script)}`,
1360
+ `rmdir ${quote(folder)}`,
1361
+ `export PATH=${quote(process.env.PATH ?? "")}`,
1362
+ `cd ${quote(cwd)} || exit 1`,
1363
+ `exec ${quote(command)} ${quote(prompt)}`
1364
+ ] : [
1365
+ "@echo off",
1366
+ "chcp 65001 >nul",
1367
+ "setlocal DisableDelayedExpansion",
1368
+ `cd /d ${batchQuote(cwd)} || exit /b 1`,
1369
+ `del "%~f0" & rmdir ${batchQuote(folder)} & ${batchQuote(command)} ${batchQuote(prompt)}`
1370
+ ]).join(mac ? "\n" : "\r\n") + "\n", { mode: 448 });
1371
+ if (mac) {
1372
+ await launch("/usr/bin/open", [script], cwd);
1373
+ return;
1374
+ }
1375
+ const shell = process.env.ComSpec ?? "cmd.exe";
1376
+ await launch(shell, [
1377
+ "/d",
1378
+ "/s",
1379
+ "/c",
1380
+ `"start "" ${shell} /d /c "${script}""`
1381
+ ], folder, true);
1382
+ } catch (error) {
1383
+ await rm(folder, {
1384
+ recursive: true,
1385
+ force: true
1386
+ });
1387
+ throw error;
1388
+ }
1389
+ }
1390
+ //#endregion
1391
+ //#region src/tui/services/wizard-spellbook.ts
1392
+ function selectSkills(menu, session, program) {
1393
+ const entries = Object.values(menu.categories).flat().filter((entry) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.id));
1394
+ const skillId = program.skillId ?? session.skillId;
1395
+ const exact = entries.find((entry) => entry.id === skillId);
1396
+ if (exact) return [exact];
1397
+ const framework = session.integration;
1398
+ const referenceGroup = skillId === framework ? "integration" : skillId;
1399
+ const references = entries.filter((entry) => entry.group === referenceGroup && entry.framework === framework);
1400
+ const reference = references.find((entry) => entry.default) ?? references[0];
1401
+ if (reference) return [reference];
1402
+ const flow = program.agentFlow ?? program.id;
1403
+ const groups = /* @__PURE__ */ new Map();
1404
+ for (const entry of entries) {
1405
+ const group = entry.group;
1406
+ if (!group || group !== flow && !group.startsWith(`${flow}-`) || entry.framework && entry.framework !== framework) continue;
1407
+ if (!groups.has(group) || entry.default) groups.set(group, entry);
1408
+ }
1409
+ return [...groups.values()];
1410
+ }
1411
+ function spellbookText(session, program, skills) {
1412
+ const framework = session.frameworkConfig?.metadata;
1413
+ const runDocs = typeof program.run === "object" ? program.run.docsUrl : void 0;
1414
+ const docs = [...new Set([
1415
+ framework?.docsUrl,
1416
+ runDocs,
1417
+ POSTHOG_DOCS_URL
1418
+ ])].filter((url) => Boolean(url));
1419
+ return [
1420
+ "# Wizard spell book",
1421
+ "",
1422
+ "Complete this task in the project containing this spell book:",
1423
+ "",
1424
+ program.description,
1425
+ "",
1426
+ `Wizard program: ${program.id}`,
1427
+ ...framework ? [`Detected framework: ${framework.name}`] : [],
1428
+ "",
1429
+ "Inspect the existing project and its instructions before making changes. Check what has already been done, complete the requested task within its stated scope, then run the relevant checks and explain any remaining manual steps.",
1430
+ "",
1431
+ "Use your own coding agent and its authentication. The Wizard could not continue its inference session. No Wizard credentials are included; ask the user for any credentials the task requires and keep them out of source code.",
1432
+ "",
1433
+ "## Setup instructions",
1434
+ "",
1435
+ ...skills.length ? [
1436
+ "Read the downloaded skills and their referenced files:",
1437
+ "",
1438
+ ...skills.map((skill) => `- [${skill.id}](skills/${skill.id}/SKILL.md)`),
1439
+ "",
1440
+ "Downloaded skills may cover only part of the task. Use the official documentation below to fill any gaps."
1441
+ ] : ["No matching skills could be downloaded. Use the official documentation below to complete the task."],
1442
+ "",
1443
+ "## Official documentation",
1444
+ "",
1445
+ ...docs.map((url) => `- ${url}`),
1446
+ ""
1447
+ ].join("\n");
1448
+ }
1449
+ async function writeWizardSpellbook(session, program) {
1450
+ const parent = path.join(session.installDir, ".posthog");
1451
+ await fs.mkdir(parent, { recursive: true });
1452
+ const directory = await fs.mkdtemp(path.join(parent, "wizard-spellbook-"));
1453
+ const readme = path.join(directory, "README.md");
1454
+ await fs.writeFile(readme, spellbookText(session, program, []));
1455
+ const menu = await fetchSkillMenu(getSkillsBaseUrl(), {
1456
+ timeoutMs: 1e4,
1457
+ maxAttempts: 1
1458
+ });
1459
+ const installed = [];
1460
+ for (const skill of menu ? selectSkills(menu, session, program) : []) if ((await downloadSkill(skill, directory, {
1461
+ skillsRoot: "skills",
1462
+ scan: (dir) => scanInstalledSkill(dir, void 0)
1463
+ })).success) installed.push(skill);
1464
+ else await fs.rm(path.join(directory, "skills", skill.id), {
1465
+ recursive: true,
1466
+ force: true
1467
+ });
1468
+ await fs.writeFile(readme, spellbookText(session, program, installed));
1469
+ return {
1470
+ path: readme,
1471
+ skillsIncluded: installed.length > 0
1472
+ };
1473
+ }
1474
+ //#endregion
1475
+ //#region src/tui/screens/ExitScreen.tsx
1476
+ /**
1477
+ * ExitScreen — Final step in every program.
1478
+ *
1479
+ * Renders nothing. Immediately asks the host to end the run with 0.
1480
+ * The cleanup handler in start-tui.ts handles the exit summary line.
1481
+ */
1482
+ const ExitScreen = ({ store }) => {
1483
+ useEffect(() => {
1484
+ if (!store.mintHandoff) store.requestExit(0);
1485
+ }, []);
1486
+ return null;
1487
+ };
1488
+ //#endregion
1489
+ //#region src/tui/screens/AuthErrorScreen.tsx
1490
+ /**
1491
+ * AuthErrorScreen — Shown when the PostHog LLM Gateway returns a 401.
1492
+ *
1493
+ * Distinct causes, most specific first:
1494
+ * 0. The OAuth grant is gone — a pre-run refresh already got `invalid_grant`
1495
+ * from the token endpoint. The only branch backed by a server verdict
1496
+ * rather than inference, so it wins; re-running is the whole fix.
1497
+ * 1. Claude Code settings.json / managed-settings overrides ANTHROPIC_*
1498
+ * env vars — auth conflict. Tell the user to log out of Claude Code.
1499
+ * 2. The PostHog API key itself was rejected — bad prefix, missing scope,
1500
+ * expired, or wrong region. Don't blame Claude Code in this case.
1501
+ */
1502
+ const AuthErrorScreen = ({ store }) => {
1503
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
1504
+ useDismissOnAnyKey(() => store.requestExit(1));
1505
+ const detail = store.authErrorDetail;
1506
+ const hasSettingsConflict = detail?.hasSettingsConflict ?? true;
1507
+ const conflicts = detail?.conflicts ?? [];
1508
+ const usingManagedLogin = detail?.usingManagedLogin ?? false;
1509
+ const credentialPlaces = detail?.credentialPlaces ?? [];
1510
+ const sessionExpired = detail?.sessionExpired ?? false;
1511
+ const logFilePath = detail?.logFilePath;
1512
+ return /* @__PURE__ */ jsxs(Box, {
1513
+ flexDirection: "column",
1514
+ flexGrow: 1,
1515
+ children: [
1516
+ /* @__PURE__ */ jsxs(Text, {
1517
+ color: "red",
1518
+ bold: true,
1519
+ children: ["✘", " Authentication error"]
1520
+ }),
1521
+ sessionExpired ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1522
+ /* @__PURE__ */ jsx(Box, {
1523
+ flexDirection: "column",
1524
+ marginTop: 1,
1525
+ children: /* @__PURE__ */ jsx(Text, { children: "Your PostHog login expired while the wizard was running, so the LLM Gateway rejected it (401). Nothing on this machine is misconfigured — the session simply ran out." })
1526
+ }),
1527
+ /* @__PURE__ */ jsx(Box, {
1528
+ marginTop: 1,
1529
+ children: /* @__PURE__ */ jsx(Text, {
1530
+ dimColor: true,
1531
+ children: "Re-run the wizard and log in again:"
1532
+ })
1533
+ }),
1534
+ /* @__PURE__ */ jsx(Box, {
1535
+ flexDirection: "column",
1536
+ marginTop: 1,
1537
+ paddingLeft: 2,
1538
+ children: /* @__PURE__ */ jsx(Text, {
1539
+ color: "cyan",
1540
+ children: "npx @posthog/wizard"
1541
+ })
1542
+ }),
1543
+ /* @__PURE__ */ jsx(Box, {
1544
+ marginTop: 1,
1545
+ children: /* @__PURE__ */ jsx(Text, {
1546
+ dimColor: true,
1547
+ children: "Any files the agent already wrote are still in your project."
1548
+ })
1549
+ })
1550
+ ] }) : usingManagedLogin ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1551
+ /* @__PURE__ */ jsx(Box, {
1552
+ flexDirection: "column",
1553
+ marginTop: 1,
1554
+ children: /* @__PURE__ */ jsx(Text, { children: "Conflicting Anthropic credentials. The agent signed in with an existing Claude login instead of the PostHog token the Wizard provided, so the LLM Gateway rejected it (401)." })
1555
+ }),
1556
+ credentialPlaces.length > 0 && /* @__PURE__ */ jsxs(Box, {
1557
+ flexDirection: "column",
1558
+ marginTop: 1,
1559
+ paddingLeft: 2,
1560
+ children: [/* @__PURE__ */ jsx(Text, {
1561
+ dimColor: true,
1562
+ children: "Conflicting credentials may come from:"
1563
+ }), credentialPlaces.map((place) => /* @__PURE__ */ jsxs(Text, { children: [
1564
+ "•",
1565
+ " ",
1566
+ place
1567
+ ] }, place))]
1568
+ }),
1569
+ /* @__PURE__ */ jsx(Box, {
1570
+ marginTop: 1,
1571
+ children: /* @__PURE__ */ jsx(Text, {
1572
+ dimColor: true,
1573
+ children: "Log out of Claude Code (clears the stored login), then re-run the Wizard:"
1574
+ })
1575
+ }),
1576
+ /* @__PURE__ */ jsx(Box, {
1577
+ flexDirection: "column",
1578
+ marginTop: 1,
1579
+ paddingLeft: 2,
1580
+ children: /* @__PURE__ */ jsx(Text, {
1581
+ color: "cyan",
1582
+ children: "claude auth logout"
1583
+ })
1584
+ })
1585
+ ] }) : hasSettingsConflict ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1586
+ /* @__PURE__ */ jsx(Box, {
1587
+ flexDirection: "column",
1588
+ marginTop: 1,
1589
+ children: /* @__PURE__ */ jsx(Text, { children: "The Wizard couldn't connect to the PostHog LLM Gateway. Claude Code settings on this machine override the Wizard's credentials." })
1590
+ }),
1591
+ conflicts.length > 0 && /* @__PURE__ */ jsx(Box, {
1592
+ flexDirection: "column",
1593
+ marginTop: 1,
1594
+ paddingLeft: 2,
1595
+ children: conflicts.map((conflict) => /* @__PURE__ */ jsxs(Text, { children: [
1596
+ "•",
1597
+ " ",
1598
+ /* @__PURE__ */ jsx(Text, {
1599
+ bold: true,
1600
+ children: conflict.path
1601
+ }),
1602
+ " sets",
1603
+ " ",
1604
+ /* @__PURE__ */ jsx(Text, {
1605
+ color: "yellow",
1606
+ children: conflict.keys.join(", ")
1607
+ })
1608
+ ] }, conflict.path))
1609
+ }),
1610
+ /* @__PURE__ */ jsx(Box, {
1611
+ marginTop: 1,
1612
+ children: /* @__PURE__ */ jsx(Text, {
1613
+ dimColor: true,
1614
+ children: "Remove those keys from the file(s) above, or log out of Claude Code, then re-run the Wizard:"
1615
+ })
1616
+ }),
1617
+ /* @__PURE__ */ jsx(Box, {
1618
+ flexDirection: "column",
1619
+ marginTop: 1,
1620
+ paddingLeft: 2,
1621
+ children: /* @__PURE__ */ jsx(Text, {
1622
+ color: "cyan",
1623
+ children: "claude auth logout"
1624
+ })
1625
+ })
1626
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Box, {
1627
+ flexDirection: "column",
1628
+ marginTop: 1,
1629
+ children: /* @__PURE__ */ jsx(Text, { children: "The PostHog LLM Gateway rejected the API key. Common causes:" })
1630
+ }), /* @__PURE__ */ jsxs(Box, {
1631
+ flexDirection: "column",
1632
+ marginTop: 1,
1633
+ paddingLeft: 2,
1634
+ children: [
1635
+ /* @__PURE__ */ jsxs(Text, { children: [
1636
+ "•",
1637
+ " Wrong key type — pass a personal API key (",
1638
+ /* @__PURE__ */ jsx(Text, {
1639
+ color: "cyan",
1640
+ children: "phx_xxx"
1641
+ }),
1642
+ ")."
1643
+ ] }),
1644
+ /* @__PURE__ */ jsxs(Text, {
1645
+ dimColor: true,
1646
+ children: [" ", "pha_ is an OAuth access token, phc_ is a project key."]
1647
+ }),
1648
+ /* @__PURE__ */ jsxs(Text, { children: [
1649
+ "•",
1650
+ " Missing scope — the key needs",
1651
+ " ",
1652
+ /* @__PURE__ */ jsx(Text, {
1653
+ color: "cyan",
1654
+ children: "llm_gateway:read"
1655
+ }),
1656
+ "."
1657
+ ] }),
1658
+ /* @__PURE__ */ jsxs(Text, { children: ["•", " Expired or revoked key."] }),
1659
+ /* @__PURE__ */ jsxs(Text, { children: [
1660
+ "•",
1661
+ " Region mismatch — ",
1662
+ /* @__PURE__ */ jsx(Text, {
1663
+ color: "cyan",
1664
+ children: "--region"
1665
+ }),
1666
+ " must match where the key was issued (us vs eu)."
1667
+ ] })
1668
+ ]
1669
+ })] }),
1670
+ logFilePath && /* @__PURE__ */ jsx(Box, {
1671
+ marginTop: 1,
1672
+ children: /* @__PURE__ */ jsxs(Text, {
1673
+ dimColor: true,
1674
+ children: ["Verbose log: ", logFilePath]
1675
+ })
1676
+ }),
1677
+ /* @__PURE__ */ jsx(Box, {
1678
+ marginTop: 1,
1679
+ children: /* @__PURE__ */ jsx(Text, {
1680
+ color: Colors.muted,
1681
+ children: "Press any key to exit"
1682
+ })
1683
+ })
1684
+ ]
1685
+ });
1686
+ };
1687
+ //#endregion
1688
+ //#region src/tui/screens/SessionTimeoutScreen.tsx
1689
+ /**
1690
+ * SessionTimeoutScreen — shown when the OAuth login window expires before the
1691
+ * user completes authorization.
1692
+ *
1693
+ * Pushed as an overlay so it takes priority over the gated AuthScreen (which
1694
+ * never completes without credentials). Terminal state: any key exits, since
1695
+ * the only way forward is to re-run the wizard for a fresh login window.
1696
+ */
1697
+ const TIMEOUT_MINUTES = Math.round(OAUTH_TIMEOUT_MS / 6e4);
1698
+ const SessionTimeoutScreen = ({ store }) => {
1699
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
1700
+ useDismissOnAnyKey(() => store.requestExit(1));
1701
+ return /* @__PURE__ */ jsxs(Box, {
1702
+ flexDirection: "column",
1703
+ flexGrow: 1,
1704
+ children: [
1705
+ /* @__PURE__ */ jsxs(Text, {
1706
+ color: "red",
1707
+ bold: true,
1708
+ children: ["✘", " Login timed out"]
1709
+ }),
1710
+ /* @__PURE__ */ jsx(Box, {
1711
+ marginTop: 1,
1712
+ children: /* @__PURE__ */ jsxs(Text, { children: [
1713
+ "The OAuth link timed out after ",
1714
+ TIMEOUT_MINUTES,
1715
+ " minutes."
1716
+ ] })
1717
+ }),
1718
+ /* @__PURE__ */ jsx(Box, {
1719
+ marginTop: 1,
1720
+ children: /* @__PURE__ */ jsx(Text, { children: "Re-run the wizard to get a fresh link and try again." })
1721
+ }),
1722
+ /* @__PURE__ */ jsx(Box, {
1723
+ marginTop: 1,
1724
+ children: /* @__PURE__ */ jsx(Text, {
1725
+ color: Colors.muted,
1726
+ children: "Press any key to exit"
1727
+ })
1728
+ })
1729
+ ]
1730
+ });
1731
+ };
1732
+ //#endregion
1733
+ //#region src/tui/screens/WizardAskScreen.tsx
1734
+ /**
1735
+ * WizardAskScreen — Overlay for the `wizard_ask` MCP tool.
1736
+ *
1737
+ * Walks the agent's question list one at a time and accumulates answers.
1738
+ * When the user submits the last question, the store resolves the
1739
+ * pending request and the overlay pops, returning the agent to its run.
1740
+ */
1741
+ /**
1742
+ * Key policy for the ask overlay: Esc declines the whole request, everything
1743
+ * else is left to the focused input. Extracted so the wiring is unit-testable
1744
+ * without a live Ink render (the test suite stubs `useInput` to a no-op).
1745
+ */
1746
+ function handleAskKey(key, store) {
1747
+ if (key.escape) store.cancelPendingQuestion();
1748
+ }
1749
+ /**
1750
+ * What pressing Esc actually does, phrased for the footer hint.
1751
+ *
1752
+ * Esc declines the *whole* request — {@link WizardStore.cancelPendingQuestion}
1753
+ * builds a cancelled answer for every question, so the ones already typed are
1754
+ * discarded too. The footer used to label that "skip", which on a multi-question
1755
+ * request reads as "skip this field": the warehouse task walks a source's
1756
+ * credentials one field at a time, several of them optional, and a user who
1757
+ * pressed Esc to pass on an optional field instead threw away the whole source
1758
+ * and dropped the agent onto its browser-handoff fallback. Naming the scope
1759
+ * costs a few characters and makes the destructive key read as destructive.
1760
+ */
1761
+ function askEscapeHint(total, answered) {
1762
+ if (total <= 1) return "skip";
1763
+ if (answered <= 0) return `skip all ${total} questions`;
1764
+ return `skip all ${total} questions, discarding the ${answered} you answered`;
1765
+ }
1766
+ /**
1767
+ * Whether the overlay must mask what the user types for this question.
1768
+ *
1769
+ * A `sensitive` answer is one the wizard has already promised to treat as a
1770
+ * secret: `wizard_ask` vaults it and hands the agent an opaque `secretRef`, so
1771
+ * the raw string never enters the model's conversation. Every other boundary
1772
+ * guards it the same way — the skip events carry no free text, handoff prose is
1773
+ * kept out of telemetry — but the overlay that collects it echoed it back in
1774
+ * plain text as it was typed, which is the one place a database password or an
1775
+ * API key is read by a person other than its owner: a shared screen, a pairing
1776
+ * session, a recorded terminal.
1777
+ *
1778
+ * `kind` is checked as well as the flag. The tool already rejects `sensitive`
1779
+ * on a picker, and a picker has nothing to mask, so this keeps the predicate
1780
+ * total over a question rather than relying on that rejection.
1781
+ */
1782
+ function shouldMaskAnswer(question) {
1783
+ return question.kind === "text" && question.sensitive === true;
1784
+ }
1785
+ /**
1786
+ * Whether an answer would leave a required question effectively unanswered.
1787
+ *
1788
+ * The `wizard_ask` schema marks fields `required` (defaulting to true), but the
1789
+ * overlay used to submit whatever the input held — so pressing Enter on an empty
1790
+ * required credential field sent an empty string on to the agent. A blank host
1791
+ * or password is indistinguishable there from a real answer, so the warehouse
1792
+ * task ran on it, failed source creation, and dead-ended with no signal that the
1793
+ * user had in effect declined. Blocking the empty submit keeps the contract the
1794
+ * schema already advertises; Esc stays the way to skip. Optional fields
1795
+ * (`required === false`) are never blocked, and pickers always yield a value.
1796
+ */
1797
+ function isRequiredButEmpty(question, value) {
1798
+ if (question.required === false) return false;
1799
+ return Array.isArray(value) ? value.length === 0 : value.trim().length === 0;
1800
+ }
1801
+ const WizardAskScreen = ({ store }) => {
1802
+ useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
1803
+ const pending = store.session.pendingQuestion;
1804
+ const [index, setIndex] = useState(0);
1805
+ const [answers, setAnswers] = useState({});
1806
+ const [lastPendingId, setLastPendingId] = useState(null);
1807
+ const [requiredHint, setRequiredHint] = useState(false);
1808
+ const [linkStatus, setLinkStatus] = useState("idle");
1809
+ const richLinks = pending?.richLinks ?? false;
1810
+ const currentPrompt = pending?.questions[index]?.prompt ?? "";
1811
+ const promptUrls = richLinks ? extractUrls(currentPrompt) : [];
1812
+ const soleUrl = promptUrls.length === 1 ? promptUrls[0] : null;
1813
+ const isTextQuestion = pending?.questions[index]?.kind === "text";
1814
+ const canActOnLink = !!soleUrl && !isTextQuestion;
1815
+ const linkSplitIndex = canActOnLink && soleUrl ? currentPrompt.indexOf(soleUrl) : -1;
1816
+ const splitLink = soleUrl !== null && linkSplitIndex !== -1;
1817
+ const linkEnd = splitLink && soleUrl ? linkSplitIndex + soleUrl.length : 0;
1818
+ const promptHead = splitLink ? currentPrompt.slice(0, linkEnd) : currentPrompt;
1819
+ const linkTrailingText = splitLink ? currentPrompt.slice(linkEnd).replace(/^\s+/, "") : "";
1820
+ useEffect(() => {
1821
+ setLinkStatus("idle");
1822
+ if (!soleUrl) return;
1823
+ copyToClipboard(soleUrl);
1824
+ }, [soleUrl]);
1825
+ useKeyBindings("wizard-ask-link", canActOnLink && soleUrl ? [{
1826
+ match: "o",
1827
+ label: "o",
1828
+ action: "open in browser",
1829
+ handler: () => {
1830
+ openInBrowser(soleUrl).then((ok) => {
1831
+ if (ok) setLinkStatus("opened");
1832
+ });
1833
+ }
1834
+ }, {
1835
+ match: "c",
1836
+ label: "c",
1837
+ action: "copy link",
1838
+ handler: () => {
1839
+ copyToClipboard(soleUrl).then((ok) => {
1840
+ if (ok) setLinkStatus("copied");
1841
+ });
1842
+ }
1843
+ }] : []);
1844
+ useInput((_input, key) => handleAskKey(key, store));
1845
+ if (!pending) return null;
1846
+ if (pending.id !== lastPendingId) {
1847
+ setLastPendingId(pending.id);
1848
+ setIndex(0);
1849
+ setAnswers({});
1850
+ setRequiredHint(false);
1851
+ return null;
1852
+ }
1853
+ const question = pending.questions[index];
1854
+ if (!question) return null;
1855
+ const total = pending.questions.length;
1856
+ const progress = total > 1 ? `Question ${index + 1} of ${total}` : null;
1857
+ const escapeHint = askEscapeHint(total, index);
1858
+ const canSkipOne = question.kind === "text" && question.required === false;
1859
+ const submit = (value) => {
1860
+ if (isRequiredButEmpty(question, value)) {
1861
+ setRequiredHint(true);
1862
+ return;
1863
+ }
1864
+ setRequiredHint(false);
1865
+ const next = {
1866
+ ...answers,
1867
+ [question.id]: value
1868
+ };
1869
+ if (index + 1 < total) {
1870
+ setAnswers(next);
1871
+ setIndex(index + 1);
1872
+ store.noteAskProgress();
1873
+ return;
1874
+ }
1875
+ store.resolvePendingQuestion(next);
1876
+ };
1877
+ return /* @__PURE__ */ jsxs(ModalOverlay, {
1878
+ borderColor: Colors.accent,
1879
+ title: `${Icons.diamond} ${pending.source}`,
1880
+ titleColor: Colors.accent,
1881
+ width: 72,
1882
+ children: [
1883
+ progress && /* @__PURE__ */ jsx(Box, {
1884
+ marginBottom: 1,
1885
+ children: /* @__PURE__ */ jsx(Text, {
1886
+ dimColor: true,
1887
+ children: progress
1888
+ })
1889
+ }),
1890
+ /* @__PURE__ */ jsx(Box, {
1891
+ flexDirection: "column",
1892
+ children: pending.richLinks ? /* @__PURE__ */ jsx(LinkText, { text: promptHead }) : /* @__PURE__ */ jsx(Text, { children: question.prompt })
1893
+ }),
1894
+ canActOnLink && /* @__PURE__ */ jsx(Box, {
1895
+ marginTop: 1,
1896
+ children: linkStatus === "idle" ? /* @__PURE__ */ jsxs(Text, {
1897
+ dimColor: true,
1898
+ children: [
1899
+ "Press ",
1900
+ /* @__PURE__ */ jsx(Text, {
1901
+ color: Colors.accent,
1902
+ children: "o"
1903
+ }),
1904
+ " to open it in your browser, or ",
1905
+ /* @__PURE__ */ jsx(Text, {
1906
+ color: Colors.accent,
1907
+ children: "c"
1908
+ }),
1909
+ " to copy the link."
1910
+ ]
1911
+ }) : linkStatus === "opened" ? /* @__PURE__ */ jsxs(Text, {
1912
+ color: Colors.success,
1913
+ children: [Icons.check, " Opening the link in your browser…"]
1914
+ }) : /* @__PURE__ */ jsxs(Text, {
1915
+ color: Colors.success,
1916
+ children: [Icons.check, " Link copied to clipboard. Paste it in your browser."]
1917
+ })
1918
+ }),
1919
+ linkTrailingText && /* @__PURE__ */ jsx(Box, {
1920
+ marginTop: 1,
1921
+ children: /* @__PURE__ */ jsx(Text, { children: linkTrailingText })
1922
+ }),
1923
+ /* @__PURE__ */ jsx(Box, {
1924
+ marginTop: 1,
1925
+ children: /* @__PURE__ */ jsx(QuestionInput, {
1926
+ question,
1927
+ onSubmit: submit
1928
+ }, `${pending.id}:${question.id}`)
1929
+ }),
1930
+ requiredHint && /* @__PURE__ */ jsx(Box, {
1931
+ marginTop: 1,
1932
+ children: /* @__PURE__ */ jsxs(Text, {
1933
+ color: Colors.accent,
1934
+ children: [
1935
+ Icons.warning,
1936
+ " This field is required — type an answer, or press ESC to ",
1937
+ escapeHint,
1938
+ "."
1939
+ ]
1940
+ })
1941
+ }),
1942
+ canSkipOne && /* @__PURE__ */ jsx(Box, {
1943
+ marginTop: 1,
1944
+ children: /* @__PURE__ */ jsxs(Text, {
1945
+ dimColor: true,
1946
+ children: [
1947
+ "Optional — press ",
1948
+ /* @__PURE__ */ jsx(Text, {
1949
+ color: Colors.accent,
1950
+ children: "ENTER"
1951
+ }),
1952
+ " on an empty answer to skip just this one."
1953
+ ]
1954
+ })
1955
+ }),
1956
+ /* @__PURE__ */ jsx(Box, {
1957
+ marginTop: 1,
1958
+ children: /* @__PURE__ */ jsxs(Text, {
1959
+ dimColor: true,
1960
+ children: [
1961
+ /* @__PURE__ */ jsx(Text, {
1962
+ color: Colors.accent,
1963
+ children: "ESC"
1964
+ }),
1965
+ " ",
1966
+ escapeHint
1967
+ ]
1968
+ })
1969
+ })
1970
+ ]
1971
+ });
1972
+ };
1973
+ const QuestionInput = ({ question, onSubmit }) => {
1974
+ switch (question.kind) {
1975
+ case "single": return /* @__PURE__ */ jsx(PickerMenu, {
1976
+ options: (question.options ?? []).map((o) => ({
1977
+ label: o.label,
1978
+ value: o.value
1979
+ })),
1980
+ onSelect: (value) => {
1981
+ onSubmit(Array.isArray(value) ? value[0] : value);
1982
+ }
1983
+ });
1984
+ case "multi": {
1985
+ const multiOptions = (question.options ?? []).map((o) => ({
1986
+ label: o.label,
1987
+ value: o.value,
1988
+ description: o.description
1989
+ }));
1990
+ return /* @__PURE__ */ jsx(PickerMenu, {
1991
+ mode: "multi",
1992
+ optionMarginBottom: multiOptions.some((o) => o.description) ? 1 : 0,
1993
+ options: multiOptions,
1994
+ onSelect: (value) => {
1995
+ onSubmit(Array.isArray(value) ? value : [value]);
1996
+ }
1997
+ });
1998
+ }
1999
+ case "text": return /* @__PURE__ */ jsxs(Box, {
2000
+ flexDirection: "column",
2001
+ width: "100%",
2002
+ children: [/* @__PURE__ */ jsx(shouldMaskAnswer(question) ? PasswordInput : TextInput, {
2003
+ placeholder: "Type your answer",
2004
+ onSubmit: (value) => onSubmit(value)
2005
+ }), /* @__PURE__ */ jsx(Box, {
2006
+ marginTop: 1,
2007
+ width: "100%",
2008
+ justifyContent: "flex-end",
2009
+ children: /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
2010
+ color: Colors.accent,
2011
+ children: "ENTER"
2012
+ }), /* @__PURE__ */ jsx(Text, {
2013
+ dimColor: true,
2014
+ children: " submit"
2015
+ })] })
2016
+ })]
2017
+ });
2018
+ }
2019
+ };
2020
+ //#endregion
2021
+ //#region src/shared/mcp-clients/login-client.ts
2022
+ function isLoginCapable(client) {
2023
+ return typeof client === "object" && client !== null && "loginCommand" in client;
2024
+ }
2025
+ //#endregion
2026
+ //#region src/shared/mcp-clients/browser-client.ts
2027
+ function isBrowserFinishable(c) {
2028
+ return typeof c === "object" && c !== null && "connectorUrl" in c && "finishInstruction" in c;
2029
+ }
2030
+ //#endregion
2031
+ //#region src/tui/services/mcp-installer.ts
2032
+ /**
2033
+ * McpInstaller — service layer between McpScreen and MCP business logic.
2034
+ *
2035
+ * Decouples the screen from step internals. Testable, swappable,
2036
+ * no dynamic imports in React components.
2037
+ */
2038
+ /**
2039
+ * Production McpInstaller backed by real MCP client detection and installation.
2040
+ */
2041
+ function createMcpInstaller() {
2042
+ let cachedClients = [];
2043
+ return {
2044
+ async detectClients() {
2045
+ const supported = await getSupportedClients();
2046
+ cachedClients = supported.map((c) => ({
2047
+ name: c.name,
2048
+ raw: c
2049
+ }));
2050
+ return supported.map((c) => ({
2051
+ name: c.name,
2052
+ supportsPlugin: isPluginCapable(c) && c.supportsPlugin(),
2053
+ pluginBundlesMcp: isPluginCapable(c) && c.pluginBundlesMcpServer(),
2054
+ finish: isBrowserFinishable(c) ? {
2055
+ url: c.connectorUrl,
2056
+ instruction: c.finishInstruction
2057
+ } : void 0,
2058
+ loginCommand: isLoginCapable(c) && c.loginCommand(false) || void 0,
2059
+ pluginLoginCommand: isLoginCapable(c) && c.pluginLoginCommand?.() || void 0
2060
+ }));
2061
+ },
2062
+ async install(clientNames, features, apiKey) {
2063
+ const resolvedFeatures = features ?? [...ALL_FEATURE_VALUES];
2064
+ const toInstall = cachedClients.filter((c) => clientNames.includes(c.name)).map((c) => c.raw);
2065
+ if (toInstall.length === 0) {
2066
+ logToFile(`[McpInstaller] No clients matched. clientNames=${JSON.stringify(clientNames)}, cached=${JSON.stringify(cachedClients.map((c) => c.name))}`);
2067
+ return [];
2068
+ }
2069
+ const results = [];
2070
+ for (const client of toInstall) {
2071
+ const name = client.name;
2072
+ try {
2073
+ const result = await client.addServer(apiKey, resolvedFeatures, false);
2074
+ results.push(toClientResult(name, result));
2075
+ if (!result?.success) logToFile(`[McpInstaller] addServer returned success=false for ${name}: ${redactSecrets(result?.reason ?? "no reason given")}`);
2076
+ } catch (err) {
2077
+ const reason = err instanceof Error ? err.message : String(err);
2078
+ logToFile(`[McpInstaller] addServer threw for ${name}: ${redactSecrets(reason)}`);
2079
+ results.push(toClientResult(name, {
2080
+ success: false,
2081
+ reason
2082
+ }));
2083
+ }
2084
+ }
2085
+ return results;
2086
+ },
2087
+ async remove(local) {
2088
+ const installed = await getInstalledClients(local);
2089
+ if (installed.length === 0) return [];
2090
+ return removeMCPServer(installed, local);
2091
+ },
2092
+ async installPlugins(clientNames) {
2093
+ const pluginClients = getSupportedPluginClients(cachedClients.filter((c) => clientNames.includes(c.name)).map((c) => c.raw));
2094
+ const results = await installPlugins(pluginClients);
2095
+ const already = namesWithStatus(results, "unchanged");
2096
+ analytics.wizardCapture("mcp plugins installed", {
2097
+ clients: [...namesWithStatus(results, "changed"), ...already],
2098
+ already_installed: already,
2099
+ attempted: pluginClients.map((c) => c.name)
2100
+ });
2101
+ return results;
2102
+ }
2103
+ };
2104
+ }
2105
+ //#endregion
2106
+ //#region src/tui/screen-registry.tsx
2107
+ function createServices(store) {
2108
+ return {
2109
+ get logPath() {
2110
+ return path$1.resolve(getLogFilePath());
2111
+ },
2112
+ openAgent: (agent, spellbookPath) => openCodingAgent(agent, store.session.installDir, spellbookPath),
2113
+ leaveSpellbook: () => writeWizardSpellbook(store.session, getProgramConfig(store.router.activeProgram)),
2114
+ mcpInstaller: createMcpInstaller()
2115
+ };
2116
+ }
2117
+ function createScreens(store, services) {
2118
+ const programScreens = {};
2119
+ for (const program of listFlowOwners()) for (const [id, render] of Object.entries(program.screens ?? {})) programScreens[id] ??= render(store, services);
2120
+ const core = {
2121
+ ["settings-override"]: /* @__PURE__ */ jsx(SettingsOverrideScreen, { store }),
2122
+ ["managed-settings"]: /* @__PURE__ */ jsx(ManagedSettingsScreen, { store }),
2123
+ ["port-conflict"]: /* @__PURE__ */ jsx(PortConflictScreen, { store }),
2124
+ ["task-notice"]: /* @__PURE__ */ jsx(TaskNoticeScreen, { store }),
2125
+ ["manual-auth-code"]: /* @__PURE__ */ jsx(ManualAuthCodeScreen, { store }),
2126
+ ["auth-error"]: /* @__PURE__ */ jsx(AuthErrorScreen, { store }),
2127
+ ["session-timeout"]: /* @__PURE__ */ jsx(SessionTimeoutScreen, { store }),
2128
+ ["wizard-ask"]: /* @__PURE__ */ jsx(WizardAskScreen, { store }),
2129
+ ["health-check"]: /* @__PURE__ */ jsx(HealthCheckScreen, { store }),
2130
+ ["setup"]: /* @__PURE__ */ jsx(SetupScreen, { store }),
2131
+ ["auth"]: /* @__PURE__ */ jsx(AuthScreen, { store }),
2132
+ ["ai-opt-in"]: /* @__PURE__ */ jsx(AiOptInRequiredScreen, { store }),
2133
+ ["run"]: /* @__PURE__ */ jsx(RunScreen, { store }),
2134
+ ["mcp"]: /* @__PURE__ */ jsx(McpScreen, {
2135
+ store,
2136
+ installer: services.mcpInstaller
2137
+ }),
2138
+ ["slack-connect"]: /* @__PURE__ */ jsx(SlackConnectScreen, { store }),
2139
+ ["keep-skills"]: /* @__PURE__ */ jsx(KeepSkillsScreen, { store }),
2140
+ ["outro"]: /* @__PURE__ */ jsx(OutroScreen, { store }),
2141
+ ["mint-failure"]: /* @__PURE__ */ jsx(MintFailureScreen, {
2142
+ store,
2143
+ services
2144
+ }),
2145
+ ["exit"]: /* @__PURE__ */ jsx(ExitScreen, { store })
2146
+ };
2147
+ return {
2148
+ ...programScreens,
2149
+ ...core
2150
+ };
2151
+ }
2152
+ //#endregion
2153
+ //#region src/tui/auth/login.ts
2154
+ /** Log in once through OAuth for the session's launch values. */
2155
+ async function oauthLogin(session, programId, store) {
2156
+ logToFile("[agent-runner] starting OAuth");
2157
+ const data = await getOrAskForProjectData({
2158
+ store,
2159
+ signup: session.signup,
2160
+ projectId: session.projectId,
2161
+ email: session.email,
2162
+ region: session.region,
2163
+ baseUrl: session.baseUrl,
2164
+ localMcp: session.localMcp,
2165
+ programId
2166
+ });
2167
+ analytics.wizardCapture("auth complete", { project_id: data.projectId });
2168
+ return {
2169
+ posthog: {
2170
+ accessToken: data.accessToken,
2171
+ refreshToken: data.refreshToken,
2172
+ expiresAt: data.expiresAt,
2173
+ oauthClientId: data.oauthClientId,
2174
+ projectApiKey: data.projectApiKey,
2175
+ host: data.host,
2176
+ projectId: data.projectId,
2177
+ missingScopes: data.missingScopes
2178
+ },
2179
+ project: data.project,
2180
+ apiUser: data.user,
2181
+ roleAtOrganization: data.roleAtOrganization
2182
+ };
2183
+ }
2184
+ /** The OAuth login as `runProgram`'s provider, reading the store's session when it is asked. */
2185
+ function oauthCredentials(store) {
2186
+ return { resolve: (programId) => oauthLogin(store.session, programId, store) };
2187
+ }
2188
+ //#endregion
2189
+ //#region src/tui/App.tsx
2190
+ const App = ({ store }) => {
2191
+ const services = useMemo(() => createServices(store), [store]);
2192
+ return /* @__PURE__ */ jsx(ScreenContainer, {
2193
+ store,
2194
+ screens: useMemo(() => createScreens(store, services), [store, services])
2195
+ });
2196
+ };
2197
+ //#endregion
2198
+ //#region src/tui/exit-line.ts
2199
+ const RESET_ATTRS = "\x1B[0m";
2200
+ const GREEN = "\x1B[32m";
2201
+ const BOLD = "\x1B[1m";
2202
+ const DIM = "\x1B[2m";
2203
+ const YELLOW = "\x1B[33m";
2204
+ const REPORT_HEAD_BYTES = 16 * 1024;
2205
+ /**
2206
+ * Mirrors the hidden Ctrl+T HUD's tally into post-exit scrollback — but only
2207
+ * when the HUD is actually visible at exit (`store.tokenHudVisible`, which
2208
+ * defaults on in dev/test and off in production; `useDismissOnAnyKey`
2209
+ * already keeps Ctrl+T from also dismissing the outro screen underneath
2210
+ * it). A production run where the user never toggled it on shouldn't have a
2211
+ * cost number appear from nowhere once the TUI tears down. `null` when the
2212
+ * HUD is hidden, or the run never produced any usage (e.g. non-agent
2213
+ * programs).
2214
+ */
2215
+ function tokenCostLine(store) {
2216
+ if (!store.tokenHudVisible) return null;
2217
+ const usage = store.tokenUsage;
2218
+ if (totalTokenCount(usage) === 0) return null;
2219
+ return `${DIM}${usage.costIsFinal ? "Final cost" : "Cost (estimate)"}: ${formatCostUsd(usage.costUsd)} (in ${formatTokenCount(usage.inputTokens)} · out ${formatTokenCount(usage.outputTokens)} · cache read ${formatTokenCount(usage.cacheReadTokens)} · cache write ${formatTokenCount(usage.cacheCreationTokens)})${RESET_ATTRS}`;
2220
+ }
2221
+ /**
2222
+ * The one manual step the wizard can't do itself: the editor's own MCP login.
2223
+ * Echoed into scrollback like the handoff prompt — command on its own plain
2224
+ * line so a terminal can triple-click-select it.
2225
+ */
2226
+ function mcpLoginBlock(store) {
2227
+ const commands = store.mcpLoginCommands;
2228
+ if (!commands || commands.length === 0) return null;
2229
+ return `${GREEN}${BOLD}\u2714 Authenticate to finish (opens your browser):${RESET_ATTRS}\n` + commands.join("\n");
2230
+ }
2231
+ /** The report's warning items, from the published handoff or else the report file. */
2232
+ function needsAttentionBlock(store) {
2233
+ const reportFile = store.session.outroData?.reportFile;
2234
+ const markdown = store.handoffText ?? (reportFile ? readFileHead(join(store.session.installDir, reportFile), REPORT_HEAD_BYTES) : null);
2235
+ const items = markdown ? readNeedsAttention(markdown) : [];
2236
+ if (items.length === 0) return null;
2237
+ return `${YELLOW}${BOLD}⚠ ${NEEDS_ATTENTION_HEADING}:${RESET_ATTRS}\n` + items.map((item) => ` • ${item}`).join("\n");
2238
+ }
2239
+ function getExitLine(store) {
2240
+ const attention = isRunFailure(store.session) ? null : needsAttentionBlock(store);
2241
+ const body = exitSummary(store);
2242
+ return attention ? `${attention}\n\n${body}` : body;
2243
+ }
2244
+ function exitSummary(store) {
2245
+ const outro = store.session.outroData;
2246
+ const label = store.programLabel ?? "Wizard";
2247
+ const costLine = tokenCostLine(store);
2248
+ const loginBlock = mcpLoginBlock(store);
2249
+ if (isRunFailure(store.session)) {
2250
+ const spellbook = store.spellbook;
2251
+ return [
2252
+ "The wizard is unavailable. Setup has not been completed.",
2253
+ spellbook && `${DIM}Point your agent at this skill (triple-click to select):${RESET_ATTRS}\n${spellbook.path}`,
2254
+ `${DIM}${MINT_FAILURE_CONTACT}${RESET_ATTRS}\n${getLogFilePath()}`,
2255
+ loginBlock,
2256
+ costLine
2257
+ ].filter(Boolean).join("\n\n");
2258
+ }
2259
+ if (outro?.kind === "success") {
2260
+ const message = outro.message ?? `${label} completed successfully.`;
2261
+ const parts = [`${GREEN}${BOLD}✔${RESET_ATTRS} ${message}${outro.reportFile && !message.includes(outro.reportFile) ? ` Check ./${outro.reportFile} for details.` : ""}`];
2262
+ if (outro.primaryLink) parts.push(`${DIM}${outro.primaryLink.label}:${RESET_ATTRS}\n${outro.primaryLink.url}`);
2263
+ if (outro.nextSteps) {
2264
+ const bullets = outro.nextSteps.items.map((item) => `${DIM} • ${item}${RESET_ATTRS}`).join("\n");
2265
+ parts.push(`${DIM}${outro.nextSteps.heading}${RESET_ATTRS}\n${bullets}`);
2266
+ }
2267
+ if (outro.handoffPrompt) parts.push(`${DIM}Hand this to your coding agent to finish up (triple-click to select):${RESET_ATTRS}\n` + outro.handoffPrompt);
2268
+ if (loginBlock) parts.push(loginBlock);
2269
+ if (costLine) parts.push(costLine);
2270
+ return parts.join("\n\n");
2271
+ }
2272
+ const parts = loginBlock ? [loginBlock] : [`${DIM}${label} exited.${RESET_ATTRS}`];
2273
+ if (costLine) parts.push(costLine);
2274
+ return parts.join("\n\n");
2275
+ }
2276
+ //#endregion
2277
+ //#region src/tui/start-tui.ts
2278
+ /**
2279
+ * start-tui.ts — Sets up the Ink TUI renderer over a new store.
2280
+ *
2281
+ * Renders in the terminal's alternate screen buffer so the wizard
2282
+ * doesn't pollute scrollback history. On exit, the previous terminal
2283
+ * content is restored and a single exit summary line is printed.
2284
+ */
2285
+ /** Render the app for `program`. `onInterrupt` runs when Ink tears itself down on Ctrl+C; the caller ends the run. */
2286
+ function startTUI(version, program, onInterrupt) {
2287
+ if (!process.stdin.isTTY) throw new WizardError("This command needs an interactive terminal (stdin is not a TTY). Run the wizard directly in a terminal, not through a pipe or script.", void 0, ErrorCodes.CliInteractiveRequired);
2288
+ enterDarkTerminal();
2289
+ const store = new WizardStore(program);
2290
+ store.version = version;
2291
+ const { unmount: inkUnmount, waitUntilExit } = render(createElement(App, { store }));
2292
+ analytics.setTag("program_id", program);
2293
+ analytics.wizardCapture("started", { program_id: program });
2294
+ store.runInitHooks();
2295
+ process.stdin.on("error", (err) => {
2296
+ if (err.code !== "EIO") throw err;
2297
+ });
2298
+ let cleaned = false;
2299
+ const cleanup = () => {
2300
+ if (cleaned) return;
2301
+ cleaned = true;
2302
+ logToFile(`[start-tui] unmounting TUI, leaving alt screen (exitCode=${process.exitCode ?? "unset"})`);
2303
+ inkUnmount();
2304
+ releaseTerminal();
2305
+ process.stdout.write(getExitLine(store) + "\n");
2306
+ };
2307
+ process.on("exit", cleanup);
2308
+ waitUntilExit().then(() => {
2309
+ if (!cleaned) onInterrupt();
2310
+ });
2311
+ return {
2312
+ unmount: cleanup,
2313
+ store,
2314
+ waitForSetup: () => store.getGate("intro")
2315
+ };
2316
+ }
2317
+ //#endregion
2318
+ //#region src/tui/run-tool.ts
2319
+ /**
2320
+ * The TUI host for a tool: its screens on their own store, with no program
2321
+ * run, no task stream and no WizardRun. The CLI builds the launch values, owns
2322
+ * the signals and applies the exit code.
2323
+ */
2324
+ /**
2325
+ * Run `toolId`'s screens. Resolves with the code a screen ends them with
2326
+ * (after the analytics flush), an abort's, 1 on a crash, or 130 or 143 on a
2327
+ * signal. Rejects when the TUI cannot start, as with no terminal.
2328
+ */
2329
+ async function runTuiTool(toolId, launch) {
2330
+ const exit = startHostExit();
2331
+ let unmountTui = () => void 0;
2332
+ let stopping = false;
2333
+ const stop = (code) => {
2334
+ if (stopping || exit.ended) return;
2335
+ stopping = true;
2336
+ unmountTui();
2337
+ (async () => {
2338
+ try {
2339
+ await analytics.shutdown("cancelled");
2340
+ } catch {}
2341
+ exit.end(code);
2342
+ })();
2343
+ };
2344
+ const onAbort = () => stop(launch.signal.reason === "SIGTERM" ? 143 : 130);
2345
+ if (launch.signal.aborted) onAbort();
2346
+ else launch.signal.addEventListener("abort", onAbort, { once: true });
2347
+ const isDoctor = toolId === "posthog-doctor";
2348
+ const localError = isDoctor ? await localServicesError(launch.session.baseUrl) : void 0;
2349
+ if (stopping) return exit.exited;
2350
+ if (localError) {
2351
+ wizardAbort({
2352
+ present: printAbortOutro,
2353
+ exit
2354
+ }, { message: localError }).catch((error) => exit.fail(error));
2355
+ return exit.exited;
2356
+ }
2357
+ const tool = getTuiTool(toolId);
2358
+ if (!tool) throw new Error(`The ${toolId} tool has no screens.`);
2359
+ const tui = startTUI(VERSION, toolId, () => stop(130));
2360
+ unmountTui = () => tui.unmount();
2361
+ tui.store.launch(buildSession(launch.session), launch.session, isDoctor ? toolId : null);
2362
+ driveTuiTool(tui.store, tool, {
2363
+ toolId,
2364
+ signal: launch.signal
2365
+ }).then(async (code) => {
2366
+ if (stopping || exit.ended) return;
2367
+ stopping = true;
2368
+ tui.unmount();
2369
+ await flushAnalytics();
2370
+ exit.end(code);
2371
+ }, (error) => {
2372
+ if (stopping || exit.ended) return;
2373
+ stopping = true;
2374
+ logToFile("[run-wizard] FATAL:", error);
2375
+ runCleanups();
2376
+ try {
2377
+ tui.unmount();
2378
+ } catch {}
2379
+ reportFatal(error);
2380
+ exit.end(1);
2381
+ });
2382
+ return exit.exited;
2383
+ }
2384
+ /**
2385
+ * Drive a tool's screens on a mounted store: run its `start`, and resolve with
2386
+ * the code the first screen exit request carries. Rejects when `start` does.
2387
+ * The intro hands off to a tool through here too.
2388
+ */
2389
+ function driveTuiTool(store, tool, { toolId, signal }) {
2390
+ return new Promise((resolve, reject) => {
2391
+ const settle = () => {
2392
+ const code = store.exitRequest;
2393
+ if (code === null) return;
2394
+ unsubscribe();
2395
+ resolve(code);
2396
+ };
2397
+ const unsubscribe = store.subscribe(settle);
2398
+ settle();
2399
+ tool.start?.({
2400
+ store,
2401
+ signal,
2402
+ logIn: async () => {
2403
+ await logIn(toolId, store.sessions, {
2404
+ provider: oauthCredentials(store),
2405
+ signal
2406
+ });
2407
+ }
2408
+ }).catch((error) => {
2409
+ unsubscribe();
2410
+ reject(error);
2411
+ });
2412
+ });
2413
+ }
2414
+ /** Before the TUI mounts: the local dev services this run points at, if one is down. */
2415
+ function localServicesError(baseUrl) {
2416
+ const local = getLocalDev();
2417
+ return checkLocalServices({
2418
+ ...local,
2419
+ localPosthog: local.localPosthog && !baseUrl
2420
+ });
2421
+ }
2422
+ /** Print a crash with the log path and the PHW line. */
2423
+ function reportFatal(error) {
2424
+ const failure = classifyRunFailure(error);
2425
+ if (failure.coded) console.error(failure.message);
2426
+ else console.error("Wizard run failed:", error);
2427
+ console.error(`Full logs: ${getLogFilePath()}`);
2428
+ emitWizardError({
2429
+ code: failure.code,
2430
+ message: failure.message
2431
+ });
2432
+ }
2433
+ //#endregion
2434
+ export { startTUI as a, runTuiTool as i, localServicesError as n, oauthCredentials as o, reportFatal as r, driveTuiTool as t };
2435
+
2436
+ //# sourceMappingURL=run-tool-CrN2xWzU.js.map