@posthog/wizard 2.79.1 → 2.80.1

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