@posthog/wizard 2.84.1 → 2.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/{PickerMenu-BtheKi1o.js → PickerMenu-DB8MO29j.js} +14 -5
  2. package/dist/PickerMenu-DB8MO29j.js.map +1 -0
  3. package/dist/{agent-interface-D8mrarFD.js → agent-interface-DuRXDmuv.js} +18 -11
  4. package/dist/agent-interface-DuRXDmuv.js.map +1 -0
  5. package/dist/{agent-prompt-loader-Bt6mrV7q.js → agent-prompt-loader-DibvEzkO.js} +6 -6
  6. package/dist/{agent-prompt-loader-Bt6mrV7q.js.map → agent-prompt-loader-DibvEzkO.js.map} +1 -1
  7. package/dist/{analytics-CX2kvq0R.js → analytics-CZNcNpiq.js} +2 -2
  8. package/dist/{analytics-CX2kvq0R.js.map → analytics-CZNcNpiq.js.map} +1 -1
  9. package/dist/analytics-jaYrwNS5.js +2 -0
  10. package/dist/{api-BOP8gF35.js → api-C3wnIy9r.js} +2 -2
  11. package/dist/{api-BOP8gF35.js.map → api-C3wnIy9r.js.map} +1 -1
  12. package/dist/api-Cjhon9jT.js +2 -0
  13. package/dist/{api-key-login-DbrZknHp.js → api-key-login-BEoyy3Pq.js} +5 -5
  14. package/dist/{api-key-login-DbrZknHp.js.map → api-key-login-BEoyy3Pq.js.map} +1 -1
  15. package/dist/api-key-login-zfRkp4JK.js +2 -0
  16. package/dist/bin.js +1 -1
  17. package/dist/{bounded-fs-BENh-Jzc.js → bounded-fs-CQGn0HxE.js} +5 -4
  18. package/dist/bounded-fs-CQGn0HxE.js.map +1 -0
  19. package/dist/{ci-install-DnjuAl1l.js → ci-install-BG77SHEw.js} +4 -4
  20. package/dist/{ci-install-DnjuAl1l.js.map → ci-install-BG77SHEw.js.map} +1 -1
  21. package/dist/{debug-C_9xDg9j.js → debug-Bw13gmS3.js} +3 -3
  22. package/dist/debug-Bw13gmS3.js.map +1 -0
  23. package/dist/{defaults-BWLIffh5.js → defaults-CLRka8GN.js} +4 -5
  24. package/dist/{defaults-BWLIffh5.js.map → defaults-CLRka8GN.js.map} +1 -1
  25. package/dist/{family-picker-CAW_VtYf.js → family-picker-CntEtZJE.js} +2 -2
  26. package/dist/{family-picker-CAW_VtYf.js.map → family-picker-CntEtZJE.js.map} +1 -1
  27. package/dist/{gateway-C6_zPLyx.js → gateway-Cl7h4Eot.js} +9 -6
  28. package/dist/{gateway-C6_zPLyx.js.map → gateway-Cl7h4Eot.js.map} +1 -1
  29. package/dist/{headless-BIQpNAHc.js → headless-DRA4mv9n.js} +2 -2
  30. package/dist/{headless-BIQpNAHc.js.map → headless-DRA4mv9n.js.map} +1 -1
  31. package/dist/{headless-sWzDbxAu.js → headless-DZAVKFMW.js} +1 -1
  32. package/dist/{host-resolution-C3ctTKNg.js → host-resolution-B0cQUK2A.js} +4 -4
  33. package/dist/{host-resolution-C3ctTKNg.js.map → host-resolution-B0cQUK2A.js.map} +1 -1
  34. package/dist/{install-CDhljVka.js → install-C0CKn7AV.js} +1 -1
  35. package/dist/{install-Dayhuaun.js → install-IvnTzMlR.js} +137 -39
  36. package/dist/install-IvnTzMlR.js.map +1 -0
  37. package/dist/{interactive-CjbttghI.js → interactive-wm9OcauB.js} +3 -3
  38. package/dist/{interactive-CjbttghI.js.map → interactive-wm9OcauB.js.map} +1 -1
  39. package/dist/{links-BOOGSL_G.js → links-Cj6OQ9Kv.js} +3 -3
  40. package/dist/{links-BOOGSL_G.js.map → links-Cj6OQ9Kv.js.map} +1 -1
  41. package/dist/{main-MzQaWN8w.js → main-SU6W6oek.js} +20 -20
  42. package/dist/{main-MzQaWN8w.js.map → main-SU6W6oek.js.map} +1 -1
  43. package/dist/{mcp-BwlD260T.js → mcp-m46rxecB.js} +3 -3
  44. package/dist/{mcp-BwlD260T.js.map → mcp-m46rxecB.js.map} +1 -1
  45. package/dist/{mcp-prompt-streaming-CoplNPI8.js → mcp-prompt-streaming-DuDUi7nq.js} +5 -5
  46. package/dist/{mcp-prompt-streaming-CoplNPI8.js.map → mcp-prompt-streaming-DuDUi7nq.js.map} +1 -1
  47. package/dist/{models-HwzgIr2E.js → models-D6XiGoty.js} +2 -2
  48. package/dist/{models-HwzgIr2E.js.map → models-D6XiGoty.js.map} +1 -1
  49. package/dist/{oauth-session-C4JV-yA9.js → oauth-session-DvAgen9l.js} +2 -2
  50. package/dist/{oauth-session-C4JV-yA9.js.map → oauth-session-DvAgen9l.js.map} +1 -1
  51. package/dist/{orchestrator-tools-Dr4w80Tn.js → orchestrator-tools-DSKw-1S6.js} +3 -3
  52. package/dist/{orchestrator-tools-Dr4w80Tn.js.map → orchestrator-tools-DSKw-1S6.js.map} +1 -1
  53. package/dist/{package-manager-Zw0JN1Kh.js → package-manager-CPzIaeu0.js} +4 -4
  54. package/dist/{package-manager-Zw0JN1Kh.js.map → package-manager-CPzIaeu0.js.map} +1 -1
  55. package/dist/{pi-qzRJcTgu.js → pi-T8emzbHL.js} +12 -12
  56. package/dist/{pi-qzRJcTgu.js.map → pi-T8emzbHL.js.map} +1 -1
  57. package/dist/{playground-B0SpcGYQ.js → playground-DE_9g_nd.js} +4 -4
  58. package/dist/{playground-B0SpcGYQ.js.map → playground-DE_9g_nd.js.map} +1 -1
  59. package/dist/{posthog-integration-4vQjbU-f.js → posthog-integration-CuEK-hPp.js} +21 -21
  60. package/dist/{posthog-integration-4vQjbU-f.js.map → posthog-integration-CuEK-hPp.js.map} +1 -1
  61. package/dist/programs-CWXzMPOe.js +3 -0
  62. package/dist/{programs-7aIrstFy.js → programs-T4EhU20m.js} +19 -15
  63. package/dist/programs-T4EhU20m.js.map +1 -0
  64. package/dist/{provisioning-BMJ3J0Vb.js → provisioning-9_MWLl8u.js} +4 -4
  65. package/dist/{provisioning-BMJ3J0Vb.js.map → provisioning-9_MWLl8u.js.map} +1 -1
  66. package/dist/provisioning-BXahZmpw.js +2 -0
  67. package/dist/{queue-tools-D8HPaGck.js → queue-tools-jFZ5kLU1.js} +4 -4
  68. package/dist/{queue-tools-D8HPaGck.js.map → queue-tools-jFZ5kLU1.js.map} +1 -1
  69. package/dist/{results-8xNiBbaP.js → results-CXTeEIUi.js} +2 -2
  70. package/dist/{results-8xNiBbaP.js.map → results-CXTeEIUi.js.map} +1 -1
  71. package/dist/{run-Dte7AuRg.js → run-Bnu1vqZG.js} +10 -10
  72. package/dist/{run-Dte7AuRg.js.map → run-Bnu1vqZG.js.map} +1 -1
  73. package/dist/run-tool-B0kgP7oA.js +2 -0
  74. package/dist/{run-tool-sdhoPp9H.js → run-tool-DOZz2dEJ.js} +37 -30
  75. package/dist/{run-tool-sdhoPp9H.js.map → run-tool-DOZz2dEJ.js.map} +1 -1
  76. package/dist/{run-DUcJFhzP.js → run-yiqaCX7y.js} +7 -7
  77. package/dist/{run-DUcJFhzP.js.map → run-yiqaCX7y.js.map} +1 -1
  78. package/dist/{runners-DxbNGuKH.js → runners-CVFLsNvO.js} +8 -8
  79. package/dist/{runners-DxbNGuKH.js.map → runners-CVFLsNvO.js.map} +1 -1
  80. package/dist/{security-BMqK129i.js → security-Bk0aumrm.js} +5 -5
  81. package/dist/{security-BMqK129i.js.map → security-Bk0aumrm.js.map} +1 -1
  82. package/dist/{setup-utils-DBOw3lnu.js → setup-utils-D5SquGvT.js} +3 -3
  83. package/dist/{setup-utils-DBOw3lnu.js.map → setup-utils-D5SquGvT.js.map} +1 -1
  84. package/dist/{start-playground-Uf3nte-P.js → start-playground-C__Gy1yk.js} +29 -14
  85. package/dist/start-playground-C__Gy1yk.js.map +1 -0
  86. package/dist/{subagent-DYza5kN6.js → subagent-DaSkUEg7.js} +3 -3
  87. package/dist/{subagent-DYza5kN6.js.map → subagent-DaSkUEg7.js.map} +1 -1
  88. package/dist/{task-CiStV2CD.js → task-Bv_o-lNo.js} +14 -14
  89. package/dist/{task-CiStV2CD.js.map → task-Bv_o-lNo.js.map} +1 -1
  90. package/dist/{telemetry-DLLuQiCt.js → telemetry-d_03gIpQ.js} +2 -2
  91. package/dist/{telemetry-DLLuQiCt.js.map → telemetry-d_03gIpQ.js.map} +1 -1
  92. package/dist/{terminal-B4EXuZra.js → terminal-BLGYQpSO.js} +583 -341
  93. package/dist/terminal-BLGYQpSO.js.map +1 -0
  94. package/dist/{tools-M5ENAiPc.js → tools-BE4Et6sl.js} +8 -8
  95. package/dist/{tools-M5ENAiPc.js.map → tools-BE4Et6sl.js.map} +1 -1
  96. package/dist/{tools-BTAi_RrA.js → tools-C8-l1rBv.js} +14 -14
  97. package/dist/{tools-BTAi_RrA.js.map → tools-C8-l1rBv.js.map} +1 -1
  98. package/dist/{tui-DigZ-Q1i.js → tui-DivYObcb.js} +5 -5
  99. package/dist/{tui-DigZ-Q1i.js.map → tui-DivYObcb.js.map} +1 -1
  100. package/dist/{tui-DU9MmsAK.js → tui-Zdby16pT.js} +1 -1
  101. package/dist/{upload-step-VNWq-3aL.js → upload-step-BfAUAEYC.js} +3 -3
  102. package/dist/{upload-step-VNWq-3aL.js.map → upload-step-BfAUAEYC.js.map} +1 -1
  103. package/dist/{urls-Bgngk5HI.js → urls-CvKF1P3f.js} +2 -2
  104. package/dist/{urls-Bgngk5HI.js.map → urls-CvKF1P3f.js.map} +1 -1
  105. package/dist/{wizard-abort-CdKk1lpi.js → wizard-abort-DTn9HFAi.js} +4 -4
  106. package/dist/{wizard-abort-CdKk1lpi.js.map → wizard-abort-DTn9HFAi.js.map} +1 -1
  107. package/dist/{yara-hooks-DPqy6eVo.js → yara-hooks-CA_ZEpAV.js} +4 -4
  108. package/dist/{yara-hooks-DPqy6eVo.js.map → yara-hooks-CA_ZEpAV.js.map} +1 -1
  109. package/dist/{yara-hooks-DCgSsXFP.js → yara-hooks-DXIqi0Km.js} +1 -1
  110. package/package.json +1 -1
  111. package/dist/PickerMenu-BtheKi1o.js.map +0 -1
  112. package/dist/agent-interface-D8mrarFD.js.map +0 -1
  113. package/dist/analytics-DgLAjlIK.js +0 -2
  114. package/dist/api-BphZo4f2.js +0 -2
  115. package/dist/api-key-login-BEi3wymJ.js +0 -2
  116. package/dist/bounded-fs-BENh-Jzc.js.map +0 -1
  117. package/dist/debug-C_9xDg9j.js.map +0 -1
  118. package/dist/install-Dayhuaun.js.map +0 -1
  119. package/dist/programs-7aIrstFy.js.map +0 -1
  120. package/dist/programs-B9Iu3Mye.js +0 -3
  121. package/dist/provisioning-BG_lh4hk.js +0 -2
  122. package/dist/run-tool-BXpuefYa.js +0 -2
  123. package/dist/start-playground-Uf3nte-P.js.map +0 -1
  124. package/dist/terminal-B4EXuZra.js.map +0 -1
@@ -1,13 +1,13 @@
1
1
  import { n as ErrorCodes } from "./emit-BqLCdV8Y.js";
2
- import { o as logToFile } from "./debug-C_9xDg9j.js";
3
- import { ct as POSTHOG_LOCAL_URL, lt as checkLocalServices, n as analytics, ut as getLocalDev } from "./analytics-CX2kvq0R.js";
4
- import { a as PostHogDestination, c as buildSession, d as SessionStore, i as createWizardRunSync, o as createFileDestination, ot as getAuditChecks, r as TaskStreamPush, t as apiKeyCredentials, u as runProgram } from "./programs-7aIrstFy.js";
2
+ import { o as logToFile } from "./debug-Bw13gmS3.js";
3
+ import { ct as POSTHOG_LOCAL_URL, lt as checkLocalServices, n as analytics, ut as getLocalDev } from "./analytics-CZNcNpiq.js";
4
+ import { a as PostHogDestination, c as buildSession, d as SessionStore, i as createWizardRunSync, o as createFileDestination, ot as getAuditChecks, r as TaskStreamPush, t as apiKeyCredentials, u as runProgram } from "./programs-T4EhU20m.js";
5
5
  import { t as classifyRunFailure } from "./errors-BXlWujEw.js";
6
6
  import { n as printAbortOutro, t as consoleLog } from "./console-log-BMhBoX-o.js";
7
- import { m as runCleanups } from "./agent-interface-D8mrarFD.js";
8
- import { C as loadWizardFlags } from "./posthog-integration-4vQjbU-f.js";
7
+ import { m as runCleanups } from "./agent-interface-DuRXDmuv.js";
8
+ import { C as loadWizardFlags } from "./posthog-integration-CuEK-hPp.js";
9
9
  import { t as modeLabel } from "./mode-label--bJuXny5.js";
10
- import { n as startHostExit, r as wizardAbort, t as registerShutdown } from "./wizard-abort-CdKk1lpi.js";
10
+ import { n as startHostExit, r as wizardAbort, t as registerShutdown } from "./wizard-abort-DTn9HFAi.js";
11
11
  import { join } from "node:path";
12
12
  import "node:fs";
13
13
  //#region src/shared/ci-gateway.ts
@@ -243,4 +243,4 @@ async function main(config, launch, exit) {
243
243
  //#endregion
244
244
  export { runHeadless };
245
245
 
246
- //# sourceMappingURL=run-DUcJFhzP.js.map
246
+ //# sourceMappingURL=run-yiqaCX7y.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-DUcJFhzP.js","names":[],"sources":["../src/shared/ci-gateway.ts","../src/headless/renderers/progress-log.ts","../src/headless/renderers/logging-ui.ts","../src/headless/run.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { IS_PRODUCTION_BUILD, runtimeEnv } from '@env';\nimport type { GatewayCredential } from '@shared/api';\nimport type { CloudRegion } from '@utils/types';\n\n/** The pre-issued gateway token a dev or test `--ci` run uses, read once and then cleared from the environment. */\nexport function readCiGatewayCredential(\n region: CloudRegion,\n): GatewayCredential {\n if (IS_PRODUCTION_BUILD)\n throw new Error('CI gateway auth requires a non-production build');\n const path = runtimeEnv('WIZARD_CI_GATEWAY_TOKEN_FILE');\n if (!path) throw new Error('WIZARD_CI_GATEWAY_TOKEN_FILE is required for CI');\n const token = readFileSync(path, 'utf8');\n delete process.env.WIZARD_CI_GATEWAY_TOKEN_FILE;\n return {\n token,\n url:\n runtimeEnv('WIZARD_CI_GATEWAY_URL') ||\n `https://ai-gateway.${region}.posthog.com`,\n };\n}\n","/** A run's progress as log lines: one `LoggingUI` call per event that has something to print. */\nimport type { SpinnerHandle } from '@agent/types';\nimport type { ProgramProgress } from '@programs/types';\nimport type { LoggingUI } from './logging-ui';\n\nexport function logProgress(\n log: LoggingUI,\n): (progress: ProgramProgress) => void {\n let spinner: SpinnerHandle | undefined;\n return ({ event }) => {\n switch (event.kind) {\n case 'lifecycle':\n if (event.phase === 'completed') log.outro(event.message);\n return;\n case 'spinner': {\n const handle = (spinner ??= log.spinner());\n handle[event.action](event.message);\n return;\n }\n case 'log':\n log.log[event.level](event.message);\n return;\n case 'status':\n log.pushStatus(event.message);\n return;\n case 'tasks':\n log.syncTodos(event.tasks);\n return;\n case 'authError':\n log.showAuthError(event.detail);\n return;\n case 'stage':\n case 'url':\n case 'usage':\n case 'finalCost':\n case 'handoff':\n case 'completion':\n case 'binding':\n return;\n case 'activity':\n // Step lines belong to the caller that asked for them, not the run UI.\n return;\n default: {\n const unhandled: never = event;\n throw new Error(\n `Unhandled agent progress: ${JSON.stringify(unhandled)}`,\n );\n }\n }\n };\n}\n","/* eslint-disable no-console */\n/**\n * LoggingUI — Logging-only implementation for CI mode.\n * No prompts, no TUI, no interactivity. Just console output.\n */\n\nimport { consoleLog } from '@shared/console-log';\nimport { TaskStatus } from '@shared/task-status';\nimport type { AuthErrorDetail, SpinnerHandle } from '@agent/types';\n\nexport class LoggingUI {\n intro = (message: string): void => consoleLog.intro(message);\n outro = (message: string): void => consoleLog.outro(message);\n log = consoleLog.log;\n\n spinner(): SpinnerHandle {\n return {\n start(message?: string) {\n if (message) console.log(`◌ ${message}`);\n },\n stop(message?: string) {\n if (message) console.log(`● ${message}`);\n },\n message(msg?: string) {\n if (msg) console.log(`◌ ${msg}`);\n },\n };\n }\n\n pushStatus(message: string): void {\n console.log(`◇ ${message}`);\n }\n\n showAuthError(detail?: AuthErrorDetail): void {\n console.log(`✖ Authentication failed (401)`);\n if (detail?.hasSettingsConflict) {\n console.log(\n `│ Claude Code auth is conflicting with the wizard. Please try again after logging out:`,\n );\n console.log(`│ claude auth logout`);\n } else {\n console.log(\n `│ The PostHog LLM Gateway rejected the API key. Common causes:`,\n );\n console.log(\n `│ - Wrong key type: pass a personal API key (phx_xxx). pha_ is an OAuth access token, phc_ is a project key.`,\n );\n console.log(\n `│ - Missing scope: the personal API key needs the \"llm_gateway:read\" scope.`,\n );\n console.log(`│ - Expired or revoked key.`);\n console.log(\n `│ - Region mismatch: --region must match the region the key was issued in (us vs eu).`,\n );\n }\n if (detail?.logFilePath) {\n console.log(`│ Verbose log: ${detail.logFilePath}`);\n }\n }\n\n private lastTodoLine = '';\n\n syncTodos(\n todos: Array<{\n id?: string;\n source?: string;\n content: string;\n status: string;\n activeForm?: string;\n }>,\n ): void {\n const completed = todos.filter(\n (t) => t.status === TaskStatus.Completed,\n ).length;\n const active = todos.filter((t) => t.status === TaskStatus.InProgress);\n if (active.length === 0) return;\n const labels = active.map((t) => t.activeForm || t.content).join(' · ');\n const line = `◌ [${completed}/${todos.length}] ${labels}`;\n // The queue re-renders on every transition; print only what changed.\n if (line === this.lastTodoLine) return;\n this.lastTodoLine = line;\n console.log(line);\n }\n}\n","/**\n * The headless host: one program run with no screens. It builds its own\n * session store, prints progress as log lines, streams the run's state, and\n * calls `runProgram`. The CLI builds the launch values and owns the signals.\n */\nimport { join } from 'node:path';\nimport {\n apiKeyCredentials,\n buildSession,\n createFileDestination,\n createWizardRunSync,\n getAuditChecks,\n loadWizardFlags,\n PostHogDestination,\n RunOutcome,\n runProgram,\n SessionStore,\n TaskStreamPush,\n} from '@programs';\nimport type {\n ProgramConfig,\n SessionArgs,\n TaskStreamOutcome,\n} from '@programs/types';\nimport { readCiGatewayCredential } from '@shared/ci-gateway';\nimport { ErrorCodes, classifyRunFailure } from '@shared/errors';\nimport {\n checkLocalServices,\n getLocalDev,\n POSTHOG_LOCAL_URL,\n} from '@shared/local-dev';\nimport { OutroKind } from '@shared/outro';\nimport { RunPhase } from '@shared/run-state';\nimport { POSTHOG_DOCS_URL } from '@shared/constants';\nimport { analytics } from '@utils/analytics';\nimport { runCleanups } from '@utils/cleanup';\nimport { logToFile } from '@utils/debug';\nimport { printAbortOutro } from '@shared/console-log';\nimport {\n registerShutdown,\n startHostExit,\n wizardAbort,\n type HostExit,\n} from '@host/wizard-abort';\nimport { modeLabel } from './mode-label';\nimport { logProgress } from './renderers/progress-log';\nimport { LoggingUI } from './renderers/logging-ui';\n\n/**\n * The two non-interactive run modes. Both drive the same pipeline today; the\n * mode is threaded explicitly (rather than sniffed from a flag) so the dispatch\n * picks an entry point — runWizardCI vs runWizardHeadless — and this core stays\n * mode-agnostic except at the few documented forks. The string values double as\n * the analytics `build` tag, so they segment runs in analytics and on\n * LLM-gateway traces (which read analytics.build).\n */\nexport type NonInteractiveMode = 'ci' | 'headless';\n\nexport type HeadlessLaunch = {\n mode: NonInteractiveMode;\n session: SessionArgs; // launch values, flags already merged over the environment\n taskStreamLog?: string; // --task-stream-log: a path, or '' for the default one\n runId?: string; // the cloud WizardRun this run reports under\n signal: AbortSignal; // the CLI aborts it on SIGINT, SIGTERM or SIGHUP, with the signal name as the reason\n};\n\n/** Run `config` headlessly. Resolves with the exit code: 0, a failure's through `wizardAbort`, or 130 or 143 on a signal. */\nexport function runHeadless(\n config: ProgramConfig,\n launch: HeadlessLaunch,\n): Promise<number> {\n const exit = startHostExit();\n void main(config, launch, exit).catch((error: unknown) => exit.fail(error));\n return exit.exited;\n}\n\nasync function main(\n config: ProgramConfig,\n launch: HeadlessLaunch,\n exit: HostExit,\n): Promise<void> {\n const log = new LoggingUI();\n const abortHost = { present: printAbortOutro, exit };\n analytics.setTag('build', launch.mode);\n\n const store = new SessionStore(buildSession({ ...launch.session, ci: true }));\n if (config.skillId) store.update({ skillId: config.skillId });\n const { session } = store;\n\n log.intro('Welcome to the PostHog setup wizard');\n log.log.info(`Running ${config.id} in ${modeLabel(launch.mode)} mode`);\n\n // Before auth: a dead local PostHog otherwise surfaces as \"Failed to fetch\n // user data\". Aborts even non-interactively — a CI run pointed at a local\n // server that isn't there is testing nothing.\n const localServicesError = await checkLocalServices({\n ...getLocalDev(),\n localMcp: session.localMcp,\n localPosthog: session.baseUrl === POSTHOG_LOCAL_URL,\n });\n if (localServicesError) {\n await wizardAbort(abortHost, {\n code: ErrorCodes.EnvLocalServicesDown,\n message: localServicesError,\n });\n return;\n }\n\n // Headless streams run state to the PostHog backend so the web app can show\n // live progress. Headless pushes to PostHog (the web app is that run's only\n // UI); `--ci` is synthetic, so it dumps locally and pushes nothing. Telemetry\n // consent gates the push only.\n // `''` resolves to the default path, so `--ci` always dumps.\n const logTarget =\n launch.mode === 'ci' ? launch.taskStreamLog ?? '' : launch.taskStreamLog;\n const fileDestination = createFileDestination(logTarget);\n const posthogDestination =\n launch.mode === 'headless' && !session.noTelemetry\n ? new PostHogDestination({\n getCredentials: () => store.session.credentials,\n onError: (e) => logToFile('[headless task-stream]', e.message),\n })\n : null;\n const destinations = [\n ...(posthogDestination ? [posthogDestination] : []),\n ...(fileDestination ? [fileDestination] : []),\n ];\n const taskStream = new TaskStreamPush({\n store,\n getFlags: () => analytics.getCachedWizardFlags(),\n programId: config.streamWorkflowId ?? config.id,\n runSync: createWizardRunSync({\n mode: launch.mode,\n programId: config.id,\n assignedId: launch.runId,\n noTelemetry: session.noTelemetry,\n getSession: () => store.session,\n }),\n destinations,\n eventPlanPath: config.eventPlanFile\n ? join(session.installDir, config.eventPlanFile)\n : undefined,\n auditChecks: config.auditLedgerFile\n ? () => getAuditChecks(store.session)\n : undefined,\n enabled: destinations.length > 0,\n });\n taskStream.attach();\n if (fileDestination) {\n logToFile(`[task-stream] ${launch.mode} dump: ${fileDestination.path}`);\n }\n\n // wizardAbort ends the run, so flush the terminal phase before any exit.\n // No-op for CI.\n const settleStream = async (outcome: TaskStreamOutcome): Promise<void> => {\n if (outcome !== 'completed') store.setRunPhase(RunPhase.Error);\n await taskStream.shutdown(2000, outcome);\n unregisterShutdown();\n };\n const unregisterShutdown = registerShutdown((outcome) =>\n settleStream(outcome),\n );\n\n // A signal cancels the run and any pending question, then ends it 130 or 143.\n const runAbort = new AbortController();\n const onSignal = (): void => {\n runAbort.abort();\n runCleanups();\n void settleStream('cancelled').then(() =>\n wizardAbort(abortHost, {\n exitCode: launch.signal.reason === 'SIGTERM' ? 143 : 130,\n }),\n );\n };\n if (launch.signal.aborted) return onSignal();\n launch.signal.addEventListener('abort', onSignal, { once: true });\n\n const credentials = apiKeyCredentials(session.apiKey ?? '', {\n region: session.region,\n baseUrl: session.baseUrl,\n localMcp: session.localMcp,\n projectId: session.projectId,\n onInfo: (message) => log.log.info(message),\n onWarning: (message) => log.log.warn(message),\n });\n\n try {\n if (launch.mode === 'ci') {\n store.update({\n ciGateway: readCiGatewayCredential(session.region ?? 'us'),\n });\n }\n\n const result = await runProgram(\n config.id,\n { store, config },\n {\n credentials,\n onProgress: logProgress(log),\n featureFlags: loadWizardFlags,\n signal: runAbort.signal,\n },\n );\n // A signal cancelled the run; its handler settles and ends it.\n if (runAbort.signal.aborted) return;\n\n if (result.outcome === RunOutcome.Crashed) throw result.failure?.error;\n if (result.outcome !== RunOutcome.Success) {\n if (result.failure?.authErrorDetail) {\n log.showAuthError(result.failure.authErrorDetail);\n }\n // The store already holds the error outro, so the stream's last push carries its code.\n await wizardAbort(abortHost, {\n ...result.failure,\n status: result.outcome === RunOutcome.Aborted ? 'cancelled' : 'error',\n });\n return;\n }\n try {\n await analytics.shutdown('success');\n } catch (error) {\n logToFile('[agent-runner] analytics shutdown failed:', error);\n }\n await settleStream('completed');\n launch.signal.removeEventListener('abort', onSignal);\n exit.end(0);\n } catch (error) {\n if (runAbort.signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorStack =\n error instanceof Error && error.stack ? error.stack : undefined;\n logToFile(`[${launch.mode}] ERROR: ${errorMessage}`);\n if (errorStack) logToFile(`[${launch.mode}] STACK: ${errorStack}`);\n\n const debugInfo =\n store.session.debug && errorStack ? `\\n\\n${errorStack}` : '';\n const docsUrl =\n store.session.frameworkConfig?.metadata.docsUrl ??\n (typeof config.run === 'object' ? config.run.docsUrl : undefined) ??\n POSTHOG_DOCS_URL;\n // A coded failure is a decision with its own message; anything else is\n // unexpected and gets the generic framing.\n const failure = classifyRunFailure(error);\n store.setOutroData({\n kind: OutroKind.Error,\n message: errorMessage,\n errorCode: failure.code,\n });\n store.setRunPhase(RunPhase.Error);\n await wizardAbort(abortHost, {\n code: failure.code,\n message: failure.coded\n ? `${errorMessage}${debugInfo}`\n : `Something went wrong: ${errorMessage}\\n\\nYou can read the documentation at ${docsUrl} to set up manually.${debugInfo}`,\n error: error as Error,\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"run-yiqaCX7y.js","names":[],"sources":["../src/shared/ci-gateway.ts","../src/headless/renderers/progress-log.ts","../src/headless/renderers/logging-ui.ts","../src/headless/run.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { IS_PRODUCTION_BUILD, runtimeEnv } from '@env';\nimport type { GatewayCredential } from '@shared/api';\nimport type { CloudRegion } from '@utils/types';\n\n/** The pre-issued gateway token a dev or test `--ci` run uses, read once and then cleared from the environment. */\nexport function readCiGatewayCredential(\n region: CloudRegion,\n): GatewayCredential {\n if (IS_PRODUCTION_BUILD)\n throw new Error('CI gateway auth requires a non-production build');\n const path = runtimeEnv('WIZARD_CI_GATEWAY_TOKEN_FILE');\n if (!path) throw new Error('WIZARD_CI_GATEWAY_TOKEN_FILE is required for CI');\n const token = readFileSync(path, 'utf8');\n delete process.env.WIZARD_CI_GATEWAY_TOKEN_FILE;\n return {\n token,\n url:\n runtimeEnv('WIZARD_CI_GATEWAY_URL') ||\n `https://ai-gateway.${region}.posthog.com`,\n };\n}\n","/** A run's progress as log lines: one `LoggingUI` call per event that has something to print. */\nimport type { SpinnerHandle } from '@agent/types';\nimport type { ProgramProgress } from '@programs/types';\nimport type { LoggingUI } from './logging-ui';\n\nexport function logProgress(\n log: LoggingUI,\n): (progress: ProgramProgress) => void {\n let spinner: SpinnerHandle | undefined;\n return ({ event }) => {\n switch (event.kind) {\n case 'lifecycle':\n if (event.phase === 'completed') log.outro(event.message);\n return;\n case 'spinner': {\n const handle = (spinner ??= log.spinner());\n handle[event.action](event.message);\n return;\n }\n case 'log':\n log.log[event.level](event.message);\n return;\n case 'status':\n log.pushStatus(event.message);\n return;\n case 'tasks':\n log.syncTodos(event.tasks);\n return;\n case 'authError':\n log.showAuthError(event.detail);\n return;\n case 'stage':\n case 'url':\n case 'usage':\n case 'finalCost':\n case 'handoff':\n case 'completion':\n case 'binding':\n return;\n case 'activity':\n // Step lines belong to the caller that asked for them, not the run UI.\n return;\n default: {\n const unhandled: never = event;\n throw new Error(\n `Unhandled agent progress: ${JSON.stringify(unhandled)}`,\n );\n }\n }\n };\n}\n","/* eslint-disable no-console */\n/**\n * LoggingUI — Logging-only implementation for CI mode.\n * No prompts, no TUI, no interactivity. Just console output.\n */\n\nimport { consoleLog } from '@shared/console-log';\nimport { TaskStatus } from '@shared/task-status';\nimport type { AuthErrorDetail, SpinnerHandle } from '@agent/types';\n\nexport class LoggingUI {\n intro = (message: string): void => consoleLog.intro(message);\n outro = (message: string): void => consoleLog.outro(message);\n log = consoleLog.log;\n\n spinner(): SpinnerHandle {\n return {\n start(message?: string) {\n if (message) console.log(`◌ ${message}`);\n },\n stop(message?: string) {\n if (message) console.log(`● ${message}`);\n },\n message(msg?: string) {\n if (msg) console.log(`◌ ${msg}`);\n },\n };\n }\n\n pushStatus(message: string): void {\n console.log(`◇ ${message}`);\n }\n\n showAuthError(detail?: AuthErrorDetail): void {\n console.log(`✖ Authentication failed (401)`);\n if (detail?.hasSettingsConflict) {\n console.log(\n `│ Claude Code auth is conflicting with the wizard. Please try again after logging out:`,\n );\n console.log(`│ claude auth logout`);\n } else {\n console.log(\n `│ The PostHog LLM Gateway rejected the API key. Common causes:`,\n );\n console.log(\n `│ - Wrong key type: pass a personal API key (phx_xxx). pha_ is an OAuth access token, phc_ is a project key.`,\n );\n console.log(\n `│ - Missing scope: the personal API key needs the \"llm_gateway:read\" scope.`,\n );\n console.log(`│ - Expired or revoked key.`);\n console.log(\n `│ - Region mismatch: --region must match the region the key was issued in (us vs eu).`,\n );\n }\n if (detail?.logFilePath) {\n console.log(`│ Verbose log: ${detail.logFilePath}`);\n }\n }\n\n private lastTodoLine = '';\n\n syncTodos(\n todos: Array<{\n id?: string;\n source?: string;\n content: string;\n status: string;\n activeForm?: string;\n }>,\n ): void {\n const completed = todos.filter(\n (t) => t.status === TaskStatus.Completed,\n ).length;\n const active = todos.filter((t) => t.status === TaskStatus.InProgress);\n if (active.length === 0) return;\n const labels = active.map((t) => t.activeForm || t.content).join(' · ');\n const line = `◌ [${completed}/${todos.length}] ${labels}`;\n // The queue re-renders on every transition; print only what changed.\n if (line === this.lastTodoLine) return;\n this.lastTodoLine = line;\n console.log(line);\n }\n}\n","/**\n * The headless host: one program run with no screens. It builds its own\n * session store, prints progress as log lines, streams the run's state, and\n * calls `runProgram`. The CLI builds the launch values and owns the signals.\n */\nimport { join } from 'node:path';\nimport {\n apiKeyCredentials,\n buildSession,\n createFileDestination,\n createWizardRunSync,\n getAuditChecks,\n loadWizardFlags,\n PostHogDestination,\n RunOutcome,\n runProgram,\n SessionStore,\n TaskStreamPush,\n} from '@programs';\nimport type {\n ProgramConfig,\n SessionArgs,\n TaskStreamOutcome,\n} from '@programs/types';\nimport { readCiGatewayCredential } from '@shared/ci-gateway';\nimport { ErrorCodes, classifyRunFailure } from '@shared/errors';\nimport {\n checkLocalServices,\n getLocalDev,\n POSTHOG_LOCAL_URL,\n} from '@shared/local-dev';\nimport { OutroKind } from '@shared/outro';\nimport { RunPhase } from '@shared/run-state';\nimport { POSTHOG_DOCS_URL } from '@shared/constants';\nimport { analytics } from '@utils/analytics';\nimport { runCleanups } from '@utils/cleanup';\nimport { logToFile } from '@utils/debug';\nimport { printAbortOutro } from '@shared/console-log';\nimport {\n registerShutdown,\n startHostExit,\n wizardAbort,\n type HostExit,\n} from '@host/wizard-abort';\nimport { modeLabel } from './mode-label';\nimport { logProgress } from './renderers/progress-log';\nimport { LoggingUI } from './renderers/logging-ui';\n\n/**\n * The two non-interactive run modes. Both drive the same pipeline today; the\n * mode is threaded explicitly (rather than sniffed from a flag) so the dispatch\n * picks an entry point — runWizardCI vs runWizardHeadless — and this core stays\n * mode-agnostic except at the few documented forks. The string values double as\n * the analytics `build` tag, so they segment runs in analytics and on\n * LLM-gateway traces (which read analytics.build).\n */\nexport type NonInteractiveMode = 'ci' | 'headless';\n\nexport type HeadlessLaunch = {\n mode: NonInteractiveMode;\n session: SessionArgs; // launch values, flags already merged over the environment\n taskStreamLog?: string; // --task-stream-log: a path, or '' for the default one\n runId?: string; // the cloud WizardRun this run reports under\n signal: AbortSignal; // the CLI aborts it on SIGINT, SIGTERM or SIGHUP, with the signal name as the reason\n};\n\n/** Run `config` headlessly. Resolves with the exit code: 0, a failure's through `wizardAbort`, or 130 or 143 on a signal. */\nexport function runHeadless(\n config: ProgramConfig,\n launch: HeadlessLaunch,\n): Promise<number> {\n const exit = startHostExit();\n void main(config, launch, exit).catch((error: unknown) => exit.fail(error));\n return exit.exited;\n}\n\nasync function main(\n config: ProgramConfig,\n launch: HeadlessLaunch,\n exit: HostExit,\n): Promise<void> {\n const log = new LoggingUI();\n const abortHost = { present: printAbortOutro, exit };\n analytics.setTag('build', launch.mode);\n\n const store = new SessionStore(buildSession({ ...launch.session, ci: true }));\n if (config.skillId) store.update({ skillId: config.skillId });\n const { session } = store;\n\n log.intro('Welcome to the PostHog setup wizard');\n log.log.info(`Running ${config.id} in ${modeLabel(launch.mode)} mode`);\n\n // Before auth: a dead local PostHog otherwise surfaces as \"Failed to fetch\n // user data\". Aborts even non-interactively — a CI run pointed at a local\n // server that isn't there is testing nothing.\n const localServicesError = await checkLocalServices({\n ...getLocalDev(),\n localMcp: session.localMcp,\n localPosthog: session.baseUrl === POSTHOG_LOCAL_URL,\n });\n if (localServicesError) {\n await wizardAbort(abortHost, {\n code: ErrorCodes.EnvLocalServicesDown,\n message: localServicesError,\n });\n return;\n }\n\n // Headless streams run state to the PostHog backend so the web app can show\n // live progress. Headless pushes to PostHog (the web app is that run's only\n // UI); `--ci` is synthetic, so it dumps locally and pushes nothing. Telemetry\n // consent gates the push only.\n // `''` resolves to the default path, so `--ci` always dumps.\n const logTarget =\n launch.mode === 'ci' ? launch.taskStreamLog ?? '' : launch.taskStreamLog;\n const fileDestination = createFileDestination(logTarget);\n const posthogDestination =\n launch.mode === 'headless' && !session.noTelemetry\n ? new PostHogDestination({\n getCredentials: () => store.session.credentials,\n onError: (e) => logToFile('[headless task-stream]', e.message),\n })\n : null;\n const destinations = [\n ...(posthogDestination ? [posthogDestination] : []),\n ...(fileDestination ? [fileDestination] : []),\n ];\n const taskStream = new TaskStreamPush({\n store,\n getFlags: () => analytics.getCachedWizardFlags(),\n programId: config.streamWorkflowId ?? config.id,\n runSync: createWizardRunSync({\n mode: launch.mode,\n programId: config.id,\n assignedId: launch.runId,\n noTelemetry: session.noTelemetry,\n getSession: () => store.session,\n }),\n destinations,\n eventPlanPath: config.eventPlanFile\n ? join(session.installDir, config.eventPlanFile)\n : undefined,\n auditChecks: config.auditLedgerFile\n ? () => getAuditChecks(store.session)\n : undefined,\n enabled: destinations.length > 0,\n });\n taskStream.attach();\n if (fileDestination) {\n logToFile(`[task-stream] ${launch.mode} dump: ${fileDestination.path}`);\n }\n\n // wizardAbort ends the run, so flush the terminal phase before any exit.\n // No-op for CI.\n const settleStream = async (outcome: TaskStreamOutcome): Promise<void> => {\n if (outcome !== 'completed') store.setRunPhase(RunPhase.Error);\n await taskStream.shutdown(2000, outcome);\n unregisterShutdown();\n };\n const unregisterShutdown = registerShutdown((outcome) =>\n settleStream(outcome),\n );\n\n // A signal cancels the run and any pending question, then ends it 130 or 143.\n const runAbort = new AbortController();\n const onSignal = (): void => {\n runAbort.abort();\n runCleanups();\n void settleStream('cancelled').then(() =>\n wizardAbort(abortHost, {\n exitCode: launch.signal.reason === 'SIGTERM' ? 143 : 130,\n }),\n );\n };\n if (launch.signal.aborted) return onSignal();\n launch.signal.addEventListener('abort', onSignal, { once: true });\n\n const credentials = apiKeyCredentials(session.apiKey ?? '', {\n region: session.region,\n baseUrl: session.baseUrl,\n localMcp: session.localMcp,\n projectId: session.projectId,\n onInfo: (message) => log.log.info(message),\n onWarning: (message) => log.log.warn(message),\n });\n\n try {\n if (launch.mode === 'ci') {\n store.update({\n ciGateway: readCiGatewayCredential(session.region ?? 'us'),\n });\n }\n\n const result = await runProgram(\n config.id,\n { store, config },\n {\n credentials,\n onProgress: logProgress(log),\n featureFlags: loadWizardFlags,\n signal: runAbort.signal,\n },\n );\n // A signal cancelled the run; its handler settles and ends it.\n if (runAbort.signal.aborted) return;\n\n if (result.outcome === RunOutcome.Crashed) throw result.failure?.error;\n if (result.outcome !== RunOutcome.Success) {\n if (result.failure?.authErrorDetail) {\n log.showAuthError(result.failure.authErrorDetail);\n }\n // The store already holds the error outro, so the stream's last push carries its code.\n await wizardAbort(abortHost, {\n ...result.failure,\n status: result.outcome === RunOutcome.Aborted ? 'cancelled' : 'error',\n });\n return;\n }\n try {\n await analytics.shutdown('success');\n } catch (error) {\n logToFile('[agent-runner] analytics shutdown failed:', error);\n }\n await settleStream('completed');\n launch.signal.removeEventListener('abort', onSignal);\n exit.end(0);\n } catch (error) {\n if (runAbort.signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorStack =\n error instanceof Error && error.stack ? error.stack : undefined;\n logToFile(`[${launch.mode}] ERROR: ${errorMessage}`);\n if (errorStack) logToFile(`[${launch.mode}] STACK: ${errorStack}`);\n\n const debugInfo =\n store.session.debug && errorStack ? `\\n\\n${errorStack}` : '';\n const docsUrl =\n store.session.frameworkConfig?.metadata.docsUrl ??\n (typeof config.run === 'object' ? config.run.docsUrl : undefined) ??\n POSTHOG_DOCS_URL;\n // A coded failure is a decision with its own message; anything else is\n // unexpected and gets the generic framing.\n const failure = classifyRunFailure(error);\n store.setOutroData({\n kind: OutroKind.Error,\n message: errorMessage,\n errorCode: failure.code,\n });\n store.setRunPhase(RunPhase.Error);\n await wizardAbort(abortHost, {\n code: failure.code,\n message: failure.coded\n ? `${errorMessage}${debugInfo}`\n : `Something went wrong: ${errorMessage}\\n\\nYou can read the documentation at ${docsUrl} to set up manually.${debugInfo}`,\n error: error as Error,\n });\n }\n}\n"],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import { n as ErrorCodes, t as emitWizardError } from "./emit-BqLCdV8Y.js";
2
- import { n as configureLogFileFromEnvironment, v as runtimeEnv } from "./debug-C_9xDg9j.js";
3
- import "./analytics-CX2kvq0R.js";
2
+ import { n as configureLogFileFromEnvironment, v as runtimeEnv } from "./debug-Bw13gmS3.js";
3
+ import "./analytics-CZNcNpiq.js";
4
4
  import "./errors-BXlWujEw.js";
5
- import { i as readProjectFile } from "./bounded-fs-BENh-Jzc.js";
6
- import "./setup-utils-DBOw3lnu.js";
5
+ import { i as readProjectFile } from "./bounded-fs-CQGn0HxE.js";
6
+ import "./setup-utils-D5SquGvT.js";
7
7
  import { t as consoleLog } from "./console-log-BMhBoX-o.js";
8
8
  import { t as modeLabel } from "./mode-label--bJuXny5.js";
9
- import "./headless-BIQpNAHc.js";
9
+ import "./headless-DRA4mv9n.js";
10
10
  import * as path$2 from "path";
11
11
  import path from "path";
12
12
  import { resolve as resolve$1 } from "node:path";
@@ -149,7 +149,7 @@ function tuiSessionArgs(options) {
149
149
  /** Run a full wizard program in the TUI. */
150
150
  function runWizard(config, options) {
151
151
  withSignals(async (signal) => {
152
- const { runTui } = await import("./tui-DU9MmsAK.js");
152
+ const { runTui } = await import("./tui-Zdby16pT.js");
153
153
  return runTui(config, {
154
154
  session: tuiSessionArgs(options),
155
155
  skillId: options.skillId,
@@ -209,7 +209,7 @@ function runNonInteractive(config, options, mode) {
209
209
  const env = readEnvironment();
210
210
  const installDir = path.resolve(options.installDir);
211
211
  withSignals(async (signal) => {
212
- const { runHeadless } = await import("./headless-sWzDbxAu.js");
212
+ const { runHeadless } = await import("./headless-DZAVKFMW.js");
213
213
  return runHeadless(config, {
214
214
  mode,
215
215
  session: {
@@ -268,4 +268,4 @@ function runWizardHeadless(config, options) {
268
268
  //#endregion
269
269
  export { runWizard as a, underSignals as c, readApiKeyFromEnv as i, withSignals as l, runWizardCI as n, tuiSessionArgs as o, API_KEY_HINT as r, exitWith as s, runWizardHeadless as t, isNonInteractiveEnvironment as u };
270
270
 
271
- //# sourceMappingURL=runners-DxbNGuKH.js.map
271
+ //# sourceMappingURL=runners-CVFLsNvO.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runners-DxbNGuKH.js","names":["resolve","path"],"sources":["../src/shared/utils/environment.ts","../src/cli/runners/resolve-no-telemetry.ts","../src/cli/runners/signals.ts","../src/cli/runners/run-wizard.ts","../src/shared/utils/env-api-key.ts","../src/cli/runners/run-non-interactive.ts","../src/cli/runners/run-wizard-ci.ts","../src/cli/runners/run-wizard-headless.ts"],"sourcesContent":["import readEnvModule from 'read-env';\n\nconst readEnv =\n typeof readEnvModule === 'function'\n ? readEnvModule\n : (readEnvModule as any).default;\nimport { tryGetPackageJson } from './setup-utils';\nimport type { WizardRunOptions } from './types';\nimport { boundedGlob } from './bounded-fs';\nimport { IS_DEV } from '@shared/constants';\n\nexport function isNonInteractiveEnvironment(): boolean {\n if (IS_DEV) {\n return false;\n }\n\n if (!process.stdout.isTTY || !process.stderr.isTTY) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Session fields the environment must never set, matched case-insensitively\n * against the camel-cased key `read-env` produces.\n *\n * `e2eAsk` re-wires the `wizard_ask` bridge in an otherwise non-interactive\n * run. Only the e2e TUI host may set it: a real `--ci` run has nobody to answer,\n * so every question would stall for the bridge timeout instead of failing fast\n * with an actionable error. See `shouldDisableAsk`.\n */\n// logFile is process config the CLI applies (`--log-file`), not a session value.\nconst NEVER_FROM_ENV = ['e2eAsk', 'runId', 'logFile'];\n\n/**\n * Session args from the `POSTHOG_WIZARD_*` environment variables.\n *\n * `read-env` camel-cases every prefixed variable into a key, and the CI runner\n * spreads the whole bag into `buildSession` — so adding a field to the session\n * silently adds an environment variable that sets it. Most of them are meant to\n * work that way (`POSTHOG_WIZARD_DEBUG` → `debug`); the ones in\n * {@link NEVER_FROM_ENV} are not, and are dropped here rather than at the call\n * site, so a second caller cannot reopen the door.\n */\nexport function readEnvironment(): Record<string, unknown> {\n const result = readEnv('POSTHOG_WIZARD') as Record<string, unknown>;\n\n for (const key of Object.keys(result)) {\n if (NEVER_FROM_ENV.some((k) => k.toLowerCase() === key.toLowerCase())) {\n delete result[key];\n }\n }\n\n return result;\n}\n\nexport async function detectEnvVarPrefix(\n options: WizardRunOptions,\n): Promise<string> {\n const packageJson = await tryGetPackageJson(options);\n if (!packageJson) return 'VITE_PUBLIC_';\n\n const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };\n const has = (name: string) => name in deps;\n // O(1) matches per probe: one config file existing anywhere answers it.\n const hasAnyFile = async (patterns: string[]) => {\n const matches = await boundedGlob(patterns, {\n cwd: options.installDir,\n limit: 1,\n });\n return matches.length > 0;\n };\n\n // --- Next.js\n if (has('next') || (await hasAnyFile(['**/next.config.{js,ts,mjs,cjs}']))) {\n return 'NEXT_PUBLIC_';\n }\n\n // --- Create React App\n if (\n has('react-scripts') ||\n has('create-react-app') ||\n (await hasAnyFile(['**/config-overrides.js']))\n ) {\n return 'REACT_APP_';\n }\n\n // --- Vite (vanilla, TanStack, Solid, etc.)\n // Note: Vite does not need PUBLIC_ but we use it to follow the docs, to improve the chances of an LLM getting it right.\n if (has('vite') || (await hasAnyFile(['**/vite.config.{js,ts,mjs,cjs}']))) {\n return 'VITE_PUBLIC_';\n }\n\n // --- SvelteKit\n if (\n has('@sveltejs/kit') ||\n (await hasAnyFile(['**/svelte.config.{js,ts}']))\n ) {\n return 'PUBLIC_';\n }\n\n // --- TanStack Start (uses Vite)\n if (\n has('@tanstack/start') ||\n (await hasAnyFile(['**/tanstack.config.{js,ts}']))\n ) {\n return 'VITE_PUBLIC_';\n }\n\n // --- SolidStart (uses Vite)\n if (has('solid-start') || (await hasAnyFile(['**/solid.config.{js,ts}']))) {\n return 'VITE_PUBLIC_';\n }\n\n // --- Astro\n if (has('astro') || (await hasAnyFile(['**/astro.config.{js,ts,mjs}']))) {\n return 'PUBLIC_';\n }\n\n // We default to Vite if we can't detect a specific framework, since it's the most commonly used.\n return 'VITE_PUBLIC_';\n}\n","/**\n * `--no-telemetry` flips `telemetry: false` via yargs negation;\n * `POSTHOG_WIZARD_NO_TELEMETRY` is honoured separately so the env-var\n * form documented in the README keeps working.\n */\nexport function resolveNoTelemetry(options: Record<string, unknown>): boolean {\n if (options.telemetry === false) return true;\n const env = process.env.POSTHOG_WIZARD_NO_TELEMETRY;\n if (env == null || env === '') return false;\n const norm = env.toLowerCase();\n return norm !== '0' && norm !== 'false';\n}\n","/** The CLI owns the process: SIGINT, SIGTERM and SIGHUP abort the host's signal, and the host's code is the exit. */\nimport { ErrorCodes, emitWizardError } from '@shared/errors';\n\nconst drain = (stream: NodeJS.WriteStream): Promise<void> =>\n new Promise((resolve) => stream.write('', () => resolve()));\n\n/** Exit once stdout and stderr have flushed, so a slow pipe keeps the whole output. */\nasync function flushThenExit(code: number): Promise<void> {\n await Promise.all([drain(process.stdout), drain(process.stderr)]);\n process.exit(code);\n}\n\n/**\n * Exit with the code `run` resolves; the only way a host's run ends the\n * process. A rejection prints the PHW error line and exits 1.\n */\nexport function exitWith(run: () => Promise<number>): void {\n void run().then(\n (code) => flushThenExit(code),\n (error: unknown) => {\n emitWizardError({\n code: ErrorCodes.InternalUnhandled,\n message: error instanceof Error ? error.message : String(error),\n });\n return flushThenExit(1);\n },\n );\n}\n\n/**\n * Run a host with a signal SIGINT, SIGTERM and SIGHUP abort (the reason is the\n * signal name), and settle as it does. A closed terminal sends SIGHUP, which\n * the hosts end like Ctrl-C. The listeners go once it settles, so what runs\n * after it, such as a fallback with no screens, ends on Ctrl-C as Node does.\n */\nexport async function underSignals(\n run: (signal: AbortSignal) => Promise<number>,\n): Promise<number> {\n const controller = new AbortController();\n const onSignal = (name: NodeJS.Signals) => {\n if (name === 'SIGHUP') hungUp = true;\n controller.abort(name);\n };\n // Attached up front: a first touch of a stream after a hangup reopens the dead terminal and blocks.\n for (const stream of OUTPUT_STREAMS) stream.on('error', onOutputError);\n process.on('SIGINT', onSignal);\n process.on('SIGTERM', onSignal);\n process.on('SIGHUP', onSignal);\n try {\n return await run(controller.signal);\n } finally {\n process.off('SIGINT', onSignal);\n process.off('SIGTERM', onSignal);\n process.off('SIGHUP', onSignal);\n // After a hangup the exit drain still writes to the dead terminal, so the guard stays.\n if (!hungUp) {\n for (const stream of OUTPUT_STREAMS) stream.off('error', onOutputError);\n }\n }\n}\n\nconst OUTPUT_STREAMS = [process.stdout, process.stderr];\nlet hungUp = false;\n\n/** A write to a closed terminal fails; that ends nothing, so the run still reports its end. */\nfunction onOutputError(error: NodeJS.ErrnoException): void {\n // EIO can land before the SIGHUP does, and only a gone terminal raises it.\n if (error.code === 'EIO') return;\n if (hungUp && HANGUP_WRITE_ERRORS.has(error.code ?? '')) return;\n throw error;\n}\n\nconst HANGUP_WRITE_ERRORS = new Set(['EPIPE', 'ERR_STREAM_DESTROYED']);\n\n/** Run a host under the signals, then exit with the code it resolves. */\nexport function withSignals(\n run: (signal: AbortSignal) => Promise<number>,\n): void {\n exitWith(() => underSignals(run));\n}\n","/** The TUI command runner: parse the launch values, start the TUI host, and exit with its code. */\nimport type { ProgramConfig, SessionArgs } from '@programs/types';\nimport type { Harness, Sequence } from '@shared/constants';\nimport { resolve } from 'node:path';\nimport { runtimeEnv } from '@env';\nimport { resolveNoTelemetry } from './resolve-no-telemetry';\nimport { withSignals } from './signals';\n\n/** The TUI session a command's parsed options launch: a program's run or a tool's screens. */\nexport function tuiSessionArgs(\n options: Record<string, unknown>,\n): SessionArgs & { integrate?: boolean } {\n return {\n debug: options.debug as boolean | undefined,\n localDev: options.localDev as boolean | undefined,\n localMcp: options.localMcp as boolean | undefined,\n localPosthog: options.localPosthog as boolean | undefined,\n installDir: resolve((options.installDir as string) || process.cwd()),\n ci: false,\n signup: options.signup as boolean | undefined,\n apiKey: options.apiKey as string | undefined,\n projectId: options.projectId as string | undefined,\n email: options.email as string | undefined,\n baseUrl: options.baseUrl as string | undefined,\n benchmark: options.benchmark as boolean | undefined,\n yaraReport: options.yaraReport as boolean | undefined,\n noTelemetry: resolveNoTelemetry(options),\n harness: options.harness as Harness | undefined,\n sequence: options.sequence as Sequence | undefined,\n model: options.model as string | undefined,\n integrate: options.integrate as boolean | undefined,\n captureAio: options.captureAio as boolean | undefined,\n };\n}\n\n/** Run a full wizard program in the TUI. */\nexport function runWizard(\n config: ProgramConfig,\n options: Record<string, unknown>,\n): void {\n withSignals(async (signal) => {\n // Loaded here, not at startup: a headless run never loads the TUI.\n const { runTui } = await import('@tui');\n return runTui(config, {\n session: tuiSessionArgs(options),\n skillId: options.skillId as string | undefined,\n taskStreamLog: options.taskStreamLog as string | undefined,\n runId:\n (options.runId as string | undefined) ??\n runtimeEnv('POSTHOG_WIZARD_RUN_ID'),\n signal,\n });\n });\n}\n","import * as path from 'path';\nimport { readProjectFile } from './bounded-fs';\n\n/**\n * Read POSTHOG_PERSONAL_API_KEY from .env.local or .env in the current\n * working directory. Returns undefined when no key is found.\n */\nexport function readApiKeyFromEnv(): string | undefined {\n for (const envFile of ['.env.local', '.env']) {\n // null when missing or unreadable, e.g. a `.env` directory (a Python virtualenv).\n const content = readProjectFile(path.join(process.cwd(), envFile));\n const match = content?.match(/^POSTHOG_PERSONAL_API_KEY=(.+)$/m);\n if (match) return match[1].trim();\n }\n return undefined;\n}\n","/** The non-interactive command runner: check the arguments, start the headless host, and exit with its code. */\nimport path from 'path';\nimport type { ProgramConfig } from '@programs/types';\nimport { modeLabel, type NonInteractiveMode } from '@headless';\nimport type { Harness, Sequence } from '@shared/constants';\nimport { ErrorCodes, emitWizardError } from '@shared/errors';\nimport type { CloudRegion } from '@utils/types';\nimport { readEnvironment } from '@utils/environment';\nimport { readApiKeyFromEnv } from '@utils/env-api-key';\nimport { runtimeEnv } from '@env';\nimport { configureLogFileFromEnvironment } from '@utils/debug';\nimport { consoleLog } from '@shared/console-log';\nimport { resolveNoTelemetry } from './resolve-no-telemetry';\nimport { withSignals } from './signals';\n\n/** The credentials every non-interactive mode accepts, for error messages. */\nexport const API_KEY_HINT =\n 'personal API key phx_xxx or wizard-app OAuth access token pha_xxx';\n\n/**\n * The single non-interactive validation layer: requires api-key and\n * install-dir. Every non-interactive entry point routes through\n * `runNonInteractive`, so this is the one place these checks live.\n */\nexport function validateNonInteractiveOptions(\n options: Record<string, unknown>,\n mode: NonInteractiveMode,\n): void {\n const label = modeLabel(mode);\n if (!options.apiKey) {\n consoleLog.intro('PostHog Wizard');\n consoleLog.log.error(`${label} mode requires --api-key (${API_KEY_HINT})`);\n emitWizardError({\n code: ErrorCodes.ArgsMissingApiKey,\n message: `${label} mode requires --api-key (${API_KEY_HINT})`,\n });\n process.exit(1);\n }\n if (!options.installDir) {\n consoleLog.intro('PostHog Wizard');\n consoleLog.log.error(\n `${label} mode requires --install-dir (directory to install in)`,\n );\n emitWizardError({\n code: ErrorCodes.ArgsMissingInstallDir,\n message: `${label} mode requires --install-dir`,\n });\n process.exit(1);\n }\n}\n\n/** Run `config` in the headless host, for CI (`runWizardCI`) and headless (`runWizardHeadless`) runs. */\nexport function runNonInteractive(\n config: ProgramConfig,\n options: Record<string, unknown>,\n mode: NonInteractiveMode,\n): void {\n validateNonInteractiveOptions(options, mode);\n configureLogFileFromEnvironment();\n\n const env = readEnvironment();\n const installDir = path.resolve(options.installDir as string);\n withSignals(async (signal) => {\n const { runHeadless } = await import('@headless');\n return runHeadless(config, {\n mode,\n session: {\n debug: options.debug as boolean | undefined,\n installDir,\n ci: true,\n signup: options.signup as boolean | undefined,\n localDev: options.localDev as boolean | undefined,\n localMcp: options.localMcp as boolean | undefined,\n localPosthog: options.localPosthog as boolean | undefined,\n apiKey: (options.apiKey as string) ?? readApiKeyFromEnv() ?? undefined,\n email: options.email as string | undefined,\n projectId: options.projectId as string | undefined,\n baseUrl: options.baseUrl as string | undefined,\n benchmark: options.benchmark as boolean | undefined,\n yaraReport: options.yaraReport as boolean | undefined,\n noTelemetry: resolveNoTelemetry(options),\n harness: options.harness as Harness | undefined,\n sequence: options.sequence as Sequence | undefined,\n model: options.model as string | undefined,\n captureAio: options.captureAio as boolean | undefined,\n ...env,\n // After the spread: yargs already resolves flag-over-env for --region,\n // so the parsed value must win over the raw env bag.\n region: (options.region ?? env.region) as CloudRegion | undefined,\n },\n taskStreamLog: options.taskStreamLog as string | undefined,\n runId:\n (options.runId as string | undefined) ??\n runtimeEnv('POSTHOG_WIZARD_RUN_ID'),\n signal,\n });\n });\n}\n","import type { ProgramConfig } from '@programs/types';\nimport { runNonInteractive } from './run-non-interactive';\n\n/**\n * CI-mode entry point (`--ci`, dev/test builds). A thin shell over the shared\n * non-interactive pipeline — see `runNonInteractive`. Kept as its own function\n * so CI and headless (`runWizardHeadless`) can diverge without re-threading the\n * many callers that route here.\n */\nexport function runWizardCI(\n config: ProgramConfig,\n options: Record<string, unknown>,\n): void {\n runNonInteractive(config, options, 'ci');\n}\n","import type { ProgramConfig } from '@programs/types';\nimport { runNonInteractive } from './run-non-interactive';\n\n/**\n * Headless entry point (the experimental published-build, non-interactive run\n * path; see @shared/headless-mode). A thin shell over the shared non-interactive\n * pipeline — see `runNonInteractive`. Today it behaves exactly like\n * `runWizardCI`; it exists as a separate function so headless can diverge later\n * (its own auth handling, telemetry, prompts, …) without touching CI or its\n * callers. Diverge by branching on the mode inside `runNonInteractive`, or by\n * giving this function its own body.\n */\nexport function runWizardHeadless(\n config: ProgramConfig,\n options: Record<string, unknown>,\n): void {\n runNonInteractive(config, options, 'headless');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runners-CVFLsNvO.js","names":["resolve","path"],"sources":["../src/shared/utils/environment.ts","../src/cli/runners/resolve-no-telemetry.ts","../src/cli/runners/signals.ts","../src/cli/runners/run-wizard.ts","../src/shared/utils/env-api-key.ts","../src/cli/runners/run-non-interactive.ts","../src/cli/runners/run-wizard-ci.ts","../src/cli/runners/run-wizard-headless.ts"],"sourcesContent":["import readEnvModule from 'read-env';\n\nconst readEnv =\n typeof readEnvModule === 'function'\n ? readEnvModule\n : (readEnvModule as any).default;\nimport { tryGetPackageJson } from './setup-utils';\nimport type { WizardRunOptions } from './types';\nimport { boundedGlob } from './bounded-fs';\nimport { IS_DEV } from '@shared/constants';\n\nexport function isNonInteractiveEnvironment(): boolean {\n if (IS_DEV) {\n return false;\n }\n\n if (!process.stdout.isTTY || !process.stderr.isTTY) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Session fields the environment must never set, matched case-insensitively\n * against the camel-cased key `read-env` produces.\n *\n * `e2eAsk` re-wires the `wizard_ask` bridge in an otherwise non-interactive\n * run. Only the e2e TUI host may set it: a real `--ci` run has nobody to answer,\n * so every question would stall for the bridge timeout instead of failing fast\n * with an actionable error. See `shouldDisableAsk`.\n */\n// logFile is process config the CLI applies (`--log-file`), not a session value.\nconst NEVER_FROM_ENV = ['e2eAsk', 'runId', 'logFile'];\n\n/**\n * Session args from the `POSTHOG_WIZARD_*` environment variables.\n *\n * `read-env` camel-cases every prefixed variable into a key, and the CI runner\n * spreads the whole bag into `buildSession` — so adding a field to the session\n * silently adds an environment variable that sets it. Most of them are meant to\n * work that way (`POSTHOG_WIZARD_DEBUG` → `debug`); the ones in\n * {@link NEVER_FROM_ENV} are not, and are dropped here rather than at the call\n * site, so a second caller cannot reopen the door.\n */\nexport function readEnvironment(): Record<string, unknown> {\n const result = readEnv('POSTHOG_WIZARD') as Record<string, unknown>;\n\n for (const key of Object.keys(result)) {\n if (NEVER_FROM_ENV.some((k) => k.toLowerCase() === key.toLowerCase())) {\n delete result[key];\n }\n }\n\n return result;\n}\n\nexport async function detectEnvVarPrefix(\n options: WizardRunOptions,\n): Promise<string> {\n const packageJson = await tryGetPackageJson(options);\n if (!packageJson) return 'VITE_PUBLIC_';\n\n const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };\n const has = (name: string) => name in deps;\n // O(1) matches per probe: one config file existing anywhere answers it.\n const hasAnyFile = async (patterns: string[]) => {\n const matches = await boundedGlob(patterns, {\n cwd: options.installDir,\n limit: 1,\n });\n return matches.length > 0;\n };\n\n // --- Next.js\n if (has('next') || (await hasAnyFile(['**/next.config.{js,ts,mjs,cjs}']))) {\n return 'NEXT_PUBLIC_';\n }\n\n // --- Create React App\n if (\n has('react-scripts') ||\n has('create-react-app') ||\n (await hasAnyFile(['**/config-overrides.js']))\n ) {\n return 'REACT_APP_';\n }\n\n // --- Vite (vanilla, TanStack, Solid, etc.)\n // Note: Vite does not need PUBLIC_ but we use it to follow the docs, to improve the chances of an LLM getting it right.\n if (has('vite') || (await hasAnyFile(['**/vite.config.{js,ts,mjs,cjs}']))) {\n return 'VITE_PUBLIC_';\n }\n\n // --- SvelteKit\n if (\n has('@sveltejs/kit') ||\n (await hasAnyFile(['**/svelte.config.{js,ts}']))\n ) {\n return 'PUBLIC_';\n }\n\n // --- TanStack Start (uses Vite)\n if (\n has('@tanstack/start') ||\n (await hasAnyFile(['**/tanstack.config.{js,ts}']))\n ) {\n return 'VITE_PUBLIC_';\n }\n\n // --- SolidStart (uses Vite)\n if (has('solid-start') || (await hasAnyFile(['**/solid.config.{js,ts}']))) {\n return 'VITE_PUBLIC_';\n }\n\n // --- Astro\n if (has('astro') || (await hasAnyFile(['**/astro.config.{js,ts,mjs}']))) {\n return 'PUBLIC_';\n }\n\n // We default to Vite if we can't detect a specific framework, since it's the most commonly used.\n return 'VITE_PUBLIC_';\n}\n","/**\n * `--no-telemetry` flips `telemetry: false` via yargs negation;\n * `POSTHOG_WIZARD_NO_TELEMETRY` is honoured separately so the env-var\n * form documented in the README keeps working.\n */\nexport function resolveNoTelemetry(options: Record<string, unknown>): boolean {\n if (options.telemetry === false) return true;\n const env = process.env.POSTHOG_WIZARD_NO_TELEMETRY;\n if (env == null || env === '') return false;\n const norm = env.toLowerCase();\n return norm !== '0' && norm !== 'false';\n}\n","/** The CLI owns the process: SIGINT, SIGTERM and SIGHUP abort the host's signal, and the host's code is the exit. */\nimport { ErrorCodes, emitWizardError } from '@shared/errors';\n\nconst drain = (stream: NodeJS.WriteStream): Promise<void> =>\n new Promise((resolve) => stream.write('', () => resolve()));\n\n/** Exit once stdout and stderr have flushed, so a slow pipe keeps the whole output. */\nasync function flushThenExit(code: number): Promise<void> {\n await Promise.all([drain(process.stdout), drain(process.stderr)]);\n process.exit(code);\n}\n\n/**\n * Exit with the code `run` resolves; the only way a host's run ends the\n * process. A rejection prints the PHW error line and exits 1.\n */\nexport function exitWith(run: () => Promise<number>): void {\n void run().then(\n (code) => flushThenExit(code),\n (error: unknown) => {\n emitWizardError({\n code: ErrorCodes.InternalUnhandled,\n message: error instanceof Error ? error.message : String(error),\n });\n return flushThenExit(1);\n },\n );\n}\n\n/**\n * Run a host with a signal SIGINT, SIGTERM and SIGHUP abort (the reason is the\n * signal name), and settle as it does. A closed terminal sends SIGHUP, which\n * the hosts end like Ctrl-C. The listeners go once it settles, so what runs\n * after it, such as a fallback with no screens, ends on Ctrl-C as Node does.\n */\nexport async function underSignals(\n run: (signal: AbortSignal) => Promise<number>,\n): Promise<number> {\n const controller = new AbortController();\n const onSignal = (name: NodeJS.Signals) => {\n if (name === 'SIGHUP') hungUp = true;\n controller.abort(name);\n };\n // Attached up front: a first touch of a stream after a hangup reopens the dead terminal and blocks.\n for (const stream of OUTPUT_STREAMS) stream.on('error', onOutputError);\n process.on('SIGINT', onSignal);\n process.on('SIGTERM', onSignal);\n process.on('SIGHUP', onSignal);\n try {\n return await run(controller.signal);\n } finally {\n process.off('SIGINT', onSignal);\n process.off('SIGTERM', onSignal);\n process.off('SIGHUP', onSignal);\n // After a hangup the exit drain still writes to the dead terminal, so the guard stays.\n if (!hungUp) {\n for (const stream of OUTPUT_STREAMS) stream.off('error', onOutputError);\n }\n }\n}\n\nconst OUTPUT_STREAMS = [process.stdout, process.stderr];\nlet hungUp = false;\n\n/** A write to a closed terminal fails; that ends nothing, so the run still reports its end. */\nfunction onOutputError(error: NodeJS.ErrnoException): void {\n // EIO can land before the SIGHUP does, and only a gone terminal raises it.\n if (error.code === 'EIO') return;\n if (hungUp && HANGUP_WRITE_ERRORS.has(error.code ?? '')) return;\n throw error;\n}\n\nconst HANGUP_WRITE_ERRORS = new Set(['EPIPE', 'ERR_STREAM_DESTROYED']);\n\n/** Run a host under the signals, then exit with the code it resolves. */\nexport function withSignals(\n run: (signal: AbortSignal) => Promise<number>,\n): void {\n exitWith(() => underSignals(run));\n}\n","/** The TUI command runner: parse the launch values, start the TUI host, and exit with its code. */\nimport type { ProgramConfig, SessionArgs } from '@programs/types';\nimport type { Harness, Sequence } from '@shared/constants';\nimport { resolve } from 'node:path';\nimport { runtimeEnv } from '@env';\nimport { resolveNoTelemetry } from './resolve-no-telemetry';\nimport { withSignals } from './signals';\n\n/** The TUI session a command's parsed options launch: a program's run or a tool's screens. */\nexport function tuiSessionArgs(\n options: Record<string, unknown>,\n): SessionArgs & { integrate?: boolean } {\n return {\n debug: options.debug as boolean | undefined,\n localDev: options.localDev as boolean | undefined,\n localMcp: options.localMcp as boolean | undefined,\n localPosthog: options.localPosthog as boolean | undefined,\n installDir: resolve((options.installDir as string) || process.cwd()),\n ci: false,\n signup: options.signup as boolean | undefined,\n apiKey: options.apiKey as string | undefined,\n projectId: options.projectId as string | undefined,\n email: options.email as string | undefined,\n baseUrl: options.baseUrl as string | undefined,\n benchmark: options.benchmark as boolean | undefined,\n yaraReport: options.yaraReport as boolean | undefined,\n noTelemetry: resolveNoTelemetry(options),\n harness: options.harness as Harness | undefined,\n sequence: options.sequence as Sequence | undefined,\n model: options.model as string | undefined,\n integrate: options.integrate as boolean | undefined,\n captureAio: options.captureAio as boolean | undefined,\n };\n}\n\n/** Run a full wizard program in the TUI. */\nexport function runWizard(\n config: ProgramConfig,\n options: Record<string, unknown>,\n): void {\n withSignals(async (signal) => {\n // Loaded here, not at startup: a headless run never loads the TUI.\n const { runTui } = await import('@tui');\n return runTui(config, {\n session: tuiSessionArgs(options),\n skillId: options.skillId as string | undefined,\n taskStreamLog: options.taskStreamLog as string | undefined,\n runId:\n (options.runId as string | undefined) ??\n runtimeEnv('POSTHOG_WIZARD_RUN_ID'),\n signal,\n });\n });\n}\n","import * as path from 'path';\nimport { readProjectFile } from './bounded-fs';\n\n/**\n * Read POSTHOG_PERSONAL_API_KEY from .env.local or .env in the current\n * working directory. Returns undefined when no key is found.\n */\nexport function readApiKeyFromEnv(): string | undefined {\n for (const envFile of ['.env.local', '.env']) {\n // null when missing or unreadable, e.g. a `.env` directory (a Python virtualenv).\n const content = readProjectFile(path.join(process.cwd(), envFile));\n const match = content?.match(/^POSTHOG_PERSONAL_API_KEY=(.+)$/m);\n if (match) return match[1].trim();\n }\n return undefined;\n}\n","/** The non-interactive command runner: check the arguments, start the headless host, and exit with its code. */\nimport path from 'path';\nimport type { ProgramConfig } from '@programs/types';\nimport { modeLabel, type NonInteractiveMode } from '@headless';\nimport type { Harness, Sequence } from '@shared/constants';\nimport { ErrorCodes, emitWizardError } from '@shared/errors';\nimport type { CloudRegion } from '@utils/types';\nimport { readEnvironment } from '@utils/environment';\nimport { readApiKeyFromEnv } from '@utils/env-api-key';\nimport { runtimeEnv } from '@env';\nimport { configureLogFileFromEnvironment } from '@utils/debug';\nimport { consoleLog } from '@shared/console-log';\nimport { resolveNoTelemetry } from './resolve-no-telemetry';\nimport { withSignals } from './signals';\n\n/** The credentials every non-interactive mode accepts, for error messages. */\nexport const API_KEY_HINT =\n 'personal API key phx_xxx or wizard-app OAuth access token pha_xxx';\n\n/**\n * The single non-interactive validation layer: requires api-key and\n * install-dir. Every non-interactive entry point routes through\n * `runNonInteractive`, so this is the one place these checks live.\n */\nexport function validateNonInteractiveOptions(\n options: Record<string, unknown>,\n mode: NonInteractiveMode,\n): void {\n const label = modeLabel(mode);\n if (!options.apiKey) {\n consoleLog.intro('PostHog Wizard');\n consoleLog.log.error(`${label} mode requires --api-key (${API_KEY_HINT})`);\n emitWizardError({\n code: ErrorCodes.ArgsMissingApiKey,\n message: `${label} mode requires --api-key (${API_KEY_HINT})`,\n });\n process.exit(1);\n }\n if (!options.installDir) {\n consoleLog.intro('PostHog Wizard');\n consoleLog.log.error(\n `${label} mode requires --install-dir (directory to install in)`,\n );\n emitWizardError({\n code: ErrorCodes.ArgsMissingInstallDir,\n message: `${label} mode requires --install-dir`,\n });\n process.exit(1);\n }\n}\n\n/** Run `config` in the headless host, for CI (`runWizardCI`) and headless (`runWizardHeadless`) runs. */\nexport function runNonInteractive(\n config: ProgramConfig,\n options: Record<string, unknown>,\n mode: NonInteractiveMode,\n): void {\n validateNonInteractiveOptions(options, mode);\n configureLogFileFromEnvironment();\n\n const env = readEnvironment();\n const installDir = path.resolve(options.installDir as string);\n withSignals(async (signal) => {\n const { runHeadless } = await import('@headless');\n return runHeadless(config, {\n mode,\n session: {\n debug: options.debug as boolean | undefined,\n installDir,\n ci: true,\n signup: options.signup as boolean | undefined,\n localDev: options.localDev as boolean | undefined,\n localMcp: options.localMcp as boolean | undefined,\n localPosthog: options.localPosthog as boolean | undefined,\n apiKey: (options.apiKey as string) ?? readApiKeyFromEnv() ?? undefined,\n email: options.email as string | undefined,\n projectId: options.projectId as string | undefined,\n baseUrl: options.baseUrl as string | undefined,\n benchmark: options.benchmark as boolean | undefined,\n yaraReport: options.yaraReport as boolean | undefined,\n noTelemetry: resolveNoTelemetry(options),\n harness: options.harness as Harness | undefined,\n sequence: options.sequence as Sequence | undefined,\n model: options.model as string | undefined,\n captureAio: options.captureAio as boolean | undefined,\n ...env,\n // After the spread: yargs already resolves flag-over-env for --region,\n // so the parsed value must win over the raw env bag.\n region: (options.region ?? env.region) as CloudRegion | undefined,\n },\n taskStreamLog: options.taskStreamLog as string | undefined,\n runId:\n (options.runId as string | undefined) ??\n runtimeEnv('POSTHOG_WIZARD_RUN_ID'),\n signal,\n });\n });\n}\n","import type { ProgramConfig } from '@programs/types';\nimport { runNonInteractive } from './run-non-interactive';\n\n/**\n * CI-mode entry point (`--ci`, dev/test builds). A thin shell over the shared\n * non-interactive pipeline — see `runNonInteractive`. Kept as its own function\n * so CI and headless (`runWizardHeadless`) can diverge without re-threading the\n * many callers that route here.\n */\nexport function runWizardCI(\n config: ProgramConfig,\n options: Record<string, unknown>,\n): void {\n runNonInteractive(config, options, 'ci');\n}\n","import type { ProgramConfig } from '@programs/types';\nimport { runNonInteractive } from './run-non-interactive';\n\n/**\n * Headless entry point (the experimental published-build, non-interactive run\n * path; see @shared/headless-mode). A thin shell over the shared non-interactive\n * pipeline — see `runNonInteractive`. Today it behaves exactly like\n * `runWizardCI`; it exists as a separate function so headless can diverge later\n * (its own auth handling, telemetry, prompts, …) without touching CI or its\n * callers. Diverge by branching on the mode inside `runNonInteractive`, or by\n * giving this function its own body.\n */\nexport function runWizardHeadless(\n config: ProgramConfig,\n options: Record<string, unknown>,\n): void {\n runNonInteractive(config, options, 'headless');\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { o as logToFile } from "./debug-C_9xDg9j.js";
2
- import { n as analytics } from "./analytics-CX2kvq0R.js";
3
- import { a as wizardCanUseTool } from "./agent-interface-D8mrarFD.js";
4
- import { _ as scanVerdict, c as isWizardDocumentationPath, f as repeatBlockReason, g as publishBlockingMatch, i as createRepeatBlockTracker, p as scanAndTriage, u as recordExternalScan } from "./yara-hooks-DPqy6eVo.js";
1
+ import { o as logToFile } from "./debug-Bw13gmS3.js";
2
+ import { n as analytics } from "./analytics-CZNcNpiq.js";
3
+ import { a as wizardCanUseTool } from "./agent-interface-DuRXDmuv.js";
4
+ import { _ as scanVerdict, c as isWizardDocumentationPath, f as repeatBlockReason, g as publishBlockingMatch, i as createRepeatBlockTracker, p as scanAndTriage, u as recordExternalScan } from "./yara-hooks-CA_ZEpAV.js";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
7
  import { fileURLToPath } from "url";
@@ -354,4 +354,4 @@ function createSecurityExtension(ctx) {
354
354
  //#endregion
355
355
  export { READ_ONLY_PI_TOOLS, createSecurityExtension };
356
356
 
357
- //# sourceMappingURL=security-BMqK129i.js.map
357
+ //# sourceMappingURL=security-Bk0aumrm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"security-BMqK129i.js","names":[],"sources":["../src/agent/runner/harness/pi/security.ts"],"sourcesContent":["/**\n * Fail-closed security for the pi backend (#525). pi has no built-in\n * permission layer, so we attach an extension that intercepts every tool call\n * — built-in (bash/read/edit/write/grep) AND custom — through pi's `tool_call`\n * hook and reuses the shared tool policy: `wizardCanUseTool` (the bash fence,\n * whose one `rm` rule is project-scoped, + .env fencing) plus the YARA\n * pre-scan. A `tool_result` hook\n * post-scans output. Both fail closed: a scanner error blocks, and a critical\n * post-scan violation latches so every subsequent tool call is blocked and the\n * run terminates as a YARA violation.\n *\n * Scanning runs through @posthog/warlock via yara-hooks' `scanAndTriage` — the\n * same engine, rules, chunking, and LLM triage policy as the anthropic\n * harness. pi handlers are async (pi's ExtensionHandler accepts promises), so\n * the WASM scan awaits inline.\n *\n * This is the one fence. Subagents run their own pi session with its subagent\n * gate installed (see subagent.ts): the same fence and state, with no `rm`.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport type { LLMProvider, ScanMatch } from '@posthog/warlock';\nimport { wizardCanUseTool } from '../../../agent-interface';\nimport {\n createRepeatBlockTracker,\n isWizardDocumentationPath,\n recordExternalScan,\n repeatBlockReason,\n scanAndTriage,\n type RepeatBlockTracker,\n} from '../../../yara-hooks';\nimport {\n publishBlockingMatch,\n scanVerdict,\n type ScanContext,\n} from '../../../yara-policy';\nimport { logToFile } from '@utils/debug';\nimport { analytics } from '@utils/analytics';\n\n/** warlock ScanMatch → the report shape `recordExternalScan` expects. */\nfunction toReportViolation(m: ScanMatch) {\n return {\n rule: m.rule,\n severity: m.metadata.severity ?? 'unknown',\n category: m.metadata.category ?? 'unknown',\n description: m.metadata.description ?? '',\n };\n}\n\n/** Runaway backstop: hard cap on tool calls per (sub)agent session. */\nexport const MAX_TOOL_CALLS = 250;\n\n/**\n * Passive telemetry for an unfixed upstream failure: gpt-5.x streaming with\n * tools leaks its function-call grammar into string values, and the leak lands\n * on disk (run 9704a73e shipped `…functions.complete_task (commentary…` plus a\n * DEL byte inside a customer's global-error.tsx). Observe-only — never blocks.\n * Prior art: https://github.com/BerriAI/litellm/issues/14260 (closed stale),\n * https://community.openai.com/t/1386422 (gpt-5.6-luna, no fix).\n */\nconst TRANSPORT_LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [\n // The litellm#14260 signature, scoped to the wizard's own tool names — a\n // generic `functions.*` would false-positive on real code (Firebase).\n {\n pattern: /functions\\.(?:complete_task|enqueue_task|read_handoffs)/,\n label: 'leaked tool-call tokens',\n },\n {\n pattern: /<\\|(?:channel|constrain|message|call|end|start|return)\\|>/,\n label: 'leaked channel markers',\n },\n // C0 controls and DEL minus tab/newline/CR — never valid in source text.\n // eslint-disable-next-line no-control-regex\n { pattern: /[\\0-\\x08\\x0B\\f\\x0E-\\x1F\\x7F]/, label: 'control characters' },\n];\n\n/** Capture one event per leaking write/edit: which pattern fired and where it sat — no matched text, ever. */\nexport function observeTransportLeak(tool: string, content: string): void {\n for (const { pattern, label } of TRANSPORT_LEAK_PATTERNS) {\n const match = pattern.exec(content);\n if (!match) continue;\n analytics.wizardCapture('file content leak observed', {\n tool,\n leak: label,\n leak_offset: match.index,\n content_length: content.length,\n // End-of-string leaks are the upstream decoder signature.\n at_end: content.length - match.index < 80,\n });\n logToFile(\n `[transport-leak] observed ${tool}: ${label} at ${match.index}/${content.length}`,\n );\n return;\n }\n}\n\nexport const READ_ONLY_PI_TOOLS: ReadonlySet<string> = new Set([\n 'read',\n 'grep',\n 'find',\n 'ls',\n]);\n\nexport interface ToolGateContext {\n readOnly?: boolean;\n disallowedTools?: readonly string[];\n /** True while a wizard_ask overlay is open (interactive); blocks Write/Edit. */\n getWizardAskPending?: () => boolean;\n /** Scan-triage classifier, resolved once in bootstrap. Absent → every flagged match is acted on. */\n triageProvider?: LLMProvider;\n /**\n * Per-run tracker of YARA-blocked payloads. When the agent retries content\n * that was already blocked, the block reason escalates (\"change the code,\n * not the retry\" → \"report it and move on\"). The extension wires one in;\n * absent (tests calling `evaluateToolCall` directly) every block reads as\n * a first attempt.\n */\n repeatTracker?: RepeatBlockTracker;\n /** Project root; rm is only allowed for files resolving strictly inside it. */\n workingDirectory?: string;\n}\n\nexport interface GateDecision {\n block: boolean;\n reason?: string;\n /** End the run, don't just refuse the call. Set when a blocked publish_handoff would otherwise leave no report. */\n terminate?: boolean;\n}\n\nconst str = (v: unknown): string => (typeof v === 'string' ? v : '');\n\n/** A pi tool path as pi opens it: its `resolveToCwd` strips a leading `@` and decodes `file://`. */\nfunction piToolPath(v: unknown): string {\n const raw = str(v);\n const p = raw.startsWith('@') ? raw.slice(1) : raw;\n if (!p.startsWith('file://')) return p;\n try {\n return fileURLToPath(p);\n } catch {\n return p; // pi fails to open it too\n }\n}\n\n// Shell metacharacters that chain, substitute, or redirect. A command carrying\n// any of these is more than one action, so we never treat it as a plain `rm`.\nconst SHELL_OPERATORS = /[;&|`$(){}<>\\n'\"\\\\]/;\n\n/** True when a target resolves to a file strictly inside the project root. */\nfunction isDeletableProjectFile(\n target: string,\n root: string,\n p: typeof path,\n): boolean {\n if (target.startsWith('-')) return false; // a flag, not a file\n if (/[*?[\\]~]/.test(target)) return false; // glob / home expansion\n if (p.basename(target).startsWith('.env')) return false; // secrets\n\n const canonicalRoot = p.resolve(root);\n const resolved = p.resolve(canonicalRoot, target);\n return (\n resolved !== canonicalRoot && resolved.startsWith(canonicalRoot + p.sep)\n );\n}\n\n/**\n * A plain `rm [-f] <file...>` whose every target is a file inside the project\n * root — the deletion shape the anthropic arm permits (there bash isn't\n * allowlisted; the shared YARA destructive-delete rules catch the dangerous\n * forms). pi rescues the same shape so the fall-through YARA scan can judge\n * it, but refuses any shell operator so it can't smuggle a second command.\n * Path checks run through the host's `path` (win32 on Windows, posix elsewhere);\n * pi always executes commands via a POSIX bash, so targets use forward slashes.\n */\nexport function isScopedFileRemoval(\n command: string,\n rawRoot: string | undefined,\n p: typeof path = path,\n): boolean {\n if (!rawRoot) return false; // no root to contain against → never rescue\n const root = p.resolve(rawRoot);\n const trimmed = command.trim();\n if (SHELL_OPERATORS.test(trimmed)) return false;\n\n const [executable, ...args] = trimmed.split(/\\s+/);\n if (executable !== 'rm') return false;\n\n if (args[0] === '-f') args.shift();\n if (args.length === 0) return false;\n\n return args.every((arg) => isDeletableProjectFile(arg, root, p));\n}\n\n/** A write that removes more than this fraction of an existing file's\n * non-whitespace content reads as a destructive whole-file rewrite. */\nconst OVERWRITE_SHRINK_LIMIT = 0.3;\n\n/** Existing files below this many non-whitespace chars are stubs; replacing one\n * wholesale is legitimate, so the shrink guard leaves them alone. */\nconst OVERWRITE_MIN_CHARS = 400;\n\nconst nonWhitespaceLength = (s: string): number => s.replace(/\\s+/g, '').length;\n\n/**\n * Refuse a write that replaces an existing file with far less content than it\n * held, steering the agent back to targeted edits. Prior art — the destructive\n * whole-file rewrite is a known model failure that targeted-edit formats exist to\n * prevent: Aider's search/replace and unified-diff, OpenAI's apply_patch,\n * Anthropic's str_replace with replace_all; pi's edit tool has no replace_all, so\n * a rejected scoped edit can fall back to a lossy full write. Additive by design:\n * the wizard only adds instrumentation, so a large shrink is the rewrite, not intent.\n */\nexport function overwriteShrinkReason(\n existing: string,\n next: string,\n): string | undefined {\n const before = nonWhitespaceLength(existing);\n if (before < OVERWRITE_MIN_CHARS) return undefined;\n const after = nonWhitespaceLength(next);\n if (after >= before * (1 - OVERWRITE_SHRINK_LIMIT)) return undefined;\n const removed = Math.round((1 - after / before) * 100);\n return `This write replaces an existing file and removes ~${removed}% of its content. Make targeted edits instead of rewriting the whole file; only write a file in full when creating it.`;\n}\n\n/**\n * Read the file a write would replace and flag a destructive shrink. A path that\n * does not exist yet is a new file, so it never triggers; with no project root to\n * resolve against (unit tests calling `evaluateToolCall` bare), it stays inert.\n */\nasync function overwriteShrinkBlock(\n input: Record<string, unknown>,\n workingDirectory: string | undefined,\n): Promise<string | undefined> {\n const target = piToolPath(input.path);\n if (!workingDirectory || !target) return undefined;\n let existing: string;\n try {\n existing = await fs.promises.readFile(\n path.resolve(workingDirectory, target),\n 'utf8',\n );\n } catch {\n return undefined; // new file (ENOENT) or unreadable — nothing to preserve\n }\n return overwriteShrinkReason(existing, str(input.content));\n}\n\n/**\n * Translate a pi tool name to the claude-cased name + input the shared policy\n * expects. pi field names (from the live tool stream): bash{command},\n * read/edit/write{path}, write adds {content}, edit adds {edits}, grep{path}.\n */\nfunction toClaudePolicyCall(\n toolName: string,\n input: Record<string, unknown>,\n): { name: string; input: Record<string, unknown> } {\n switch (toolName) {\n case 'bash':\n return { name: 'Bash', input: { command: str(input.command) } };\n case 'read':\n return { name: 'Read', input: { file_path: piToolPath(input.path) } };\n case 'write':\n return { name: 'Write', input: { file_path: piToolPath(input.path) } };\n case 'edit':\n return { name: 'Edit', input: { file_path: piToolPath(input.path) } };\n case 'grep':\n return {\n name: 'Grep',\n input: { path: piToolPath(input.path), glob: input.glob },\n };\n default:\n // Custom tools (load_skill_menu, set_env_values, dispatch_agent, …) +\n // find/ls: no path/command, policy allows (their own handlers are fenced).\n return { name: toolName, input };\n }\n}\n\n/**\n * The agent-facing block message. Includes the rule's remediation so a blocked\n * agent can self-correct (e.g. \"move PII to identify()/$set\") instead of\n * retrying near-identical content.\n */\nfunction blockMessage(m: ScanMatch): string {\n const remediation = m.metadata.remediation\n ? ` Fix: ${m.metadata.remediation}.`\n : '';\n return `[YARA] ${m.rule}: ${\n m.metadata.description ?? 'security rule matched'\n }.${remediation} Blocked for security.`;\n}\n\n/**\n * YARA scan of the content a tool is about to act on, BEFORE it executes.\n * - bash → scan the command ('command'-context rules: exfiltration,\n * destructive, force-push). No triage — a flagged command always blocks,\n * same as the anthropic PreToolUse hook, so the LLM call would be wasted.\n * - write/edit → scan the content being written ('output'-context rules:\n * hardcoded keys, PII), WITH triage, and with the same wizard-doc\n * `posthog_pii` suppression the anthropic path uses so the agent's own\n * event-plan files aren't blocked.\n * - publish_handoff → scan the report ('output'-context rules) with triage,\n * but only a CRITICAL match refuses the publish — it's the only channel.\n * Returns a block reason, or undefined to allow. Read/grep are post-scanned on\n * their output (in the tool_result hook), not here.\n */\nasync function preExecutionYaraBlock(\n toolName: string,\n input: Record<string, unknown>,\n llmProvider: LLMProvider | undefined,\n repeatTracker?: RepeatBlockTracker,\n): Promise<string | undefined> {\n let content: string;\n let ctx: ScanContext;\n let triage: LLMProvider | undefined;\n // Report labels for the scan report (phase/tool), matching the anthropic\n // hooks so pi and anthropic runs read the same in telemetry.\n let phase: string;\n let tool: string;\n switch (toolName) {\n case 'bash':\n content = str(input.command);\n ctx = 'command';\n triage = undefined;\n phase = 'PreToolUse';\n tool = 'Bash';\n break;\n case 'write':\n content = str(input.content);\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Write';\n break;\n case 'edit':\n // Scan ONLY the replacement text. `edits` is [{oldText, newText}] and\n // oldText is pre-existing file content the agent is changing — scanning\n // it blocks edits whose *surroundings* contain a violation, and even\n // edits that are removing one (field FPs: capture edits adjacent to\n // existing identify() PII; pre-existing violations blocking their own\n // replacement). Matches the anthropic path, which scans new_string only.\n content = (Array.isArray(input.edits) ? input.edits : [])\n .map((e) => str((e as Record<string, unknown>)?.newText))\n .join('\\n');\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Edit';\n break;\n case 'publish_handoff':\n // Agent-authored content persisted to the task stream and the host file.\n // Own labels, not Write's — the block bar differs (critical only).\n content = str(input.content);\n ctx = 'output';\n triage = llmProvider;\n phase = 'PreToolUse';\n tool = 'publish_handoff';\n break;\n default:\n return undefined;\n }\n if (!content) return undefined;\n if (ctx === 'output') observeTransportLeak(tool, content);\n\n let matches = await scanAndTriage(content, ctx, triage);\n if (ctx === 'output' && isWizardDocumentationPath(piToolPath(input.path))) {\n matches = matches.filter((m) => m.metadata.category !== 'posthog_pii');\n }\n // Any match blocks — except publish_handoff, critical only.\n const blocking =\n toolName === 'publish_handoff'\n ? publishBlockingMatch(matches)\n : matches[0] ?? null;\n\n recordExternalScan(\n phase,\n tool,\n matches.map(toReportViolation),\n blocking ? 'blocked' : 'warned',\n );\n if (!blocking) return undefined;\n\n const attempt = repeatTracker?.attempt(toolName, content) ?? 1;\n return repeatBlockReason(attempt, toolName, blockMessage(blocking));\n}\n\n/**\n * The pure gate decision for a single tool call. Reuses `wizardCanUseTool`\n * (deny → block) then the YARA content scan (match → block). Fail-closed: any\n * thrown error blocks.\n */\nexport async function evaluateToolCall(\n toolName: string,\n input: Record<string, unknown>,\n ctx: ToolGateContext = {},\n llmProvider?: LLMProvider,\n): Promise<GateDecision> {\n try {\n if (ctx.readOnly && !READ_ONLY_PI_TOOLS.has(toolName)) {\n return {\n block: true,\n reason: `Tool ${toolName} is disabled for read-only runs.`,\n };\n }\n const policy = toClaudePolicyCall(toolName, input);\n const decision = wizardCanUseTool(policy.name, policy.input, {\n disallowedTools: ctx.disallowedTools,\n wizardAskPending: ctx.getWizardAskPending?.() ?? false,\n workingDirectory: ctx.workingDirectory,\n });\n if (decision.behavior === 'deny') {\n return { block: true, reason: decision.message };\n }\n\n const yaraReason = await preExecutionYaraBlock(\n toolName,\n input,\n llmProvider,\n ctx.repeatTracker,\n );\n if (yaraReason) return { block: true, reason: yaraReason };\n\n if (toolName === 'write') {\n const shrinkReason = await overwriteShrinkBlock(\n input,\n ctx.workingDirectory,\n );\n if (shrinkReason) return { block: true, reason: shrinkReason };\n }\n\n return { block: false };\n } catch (err) {\n logToFile('[pi-security] gate error — failing closed:', err);\n // A publish_handoff the gate can't clear leaves the run with no report at\n // all, so end it visibly instead of letting the agent reword and retry.\n return {\n block: true,\n reason: 'Security check failed; tool blocked (fail-closed).',\n terminate: toolName === 'publish_handoff',\n };\n }\n}\n\n/** pi result tool names whose OUTPUT gets scanned for 'input'-context rules. */\nfunction isPostScanTool(toolName: string): boolean {\n return toolName === 'read' || toolName === 'bash';\n}\n\n/** Mutable state the backend reads after the run to classify the outcome. */\nexport interface SecurityState {\n criticalViolation: boolean;\n blockedCount: number;\n toolCalls: number;\n}\n\n/** Options for {@link createSecurityExtension}. The root is required, so no run loses the rm allowance or the shrink guard by omission. */\nexport type SecurityExtensionOptions = ToolGateContext & {\n workingDirectory: string;\n};\n\ndeclare const subagentGate: unique symbol;\n\n/** A gate with no project root, so it never allows rm. Only `subagentFactory` makes one, so a subagent can't be handed the parent's. */\nexport type SubagentSecurityFactory = ((pi: PiExtensionApiLike) => void) & {\n readonly [subagentGate]: true;\n};\n\n/**\n * Build the pi security extension + the shared state the backend inspects.\n * Install `factory` via `extensionFactories`. Give subagent sessions\n * `subagentFactory`: the same fence and state, with no project root, so no rm.\n */\nexport function createSecurityExtension(ctx: SecurityExtensionOptions): {\n factory: (pi: PiExtensionApiLike) => void;\n subagentFactory: SubagentSecurityFactory;\n state: SecurityState;\n} {\n const state: SecurityState = {\n criticalViolation: false,\n blockedCount: 0,\n toolCalls: 0,\n };\n\n // One triage provider per extension. Undefined (no gateway auth) means\n // triage is skipped and every flagged match is acted on — fail closed.\n const llmProvider = ctx.triageProvider;\n\n // One tracker per extension = per (sub)agent session, so an identical\n // payload retried after a block gets the escalating reason.\n const gateCtx: ToolGateContext = {\n ...ctx,\n repeatTracker: ctx.repeatTracker ?? createRepeatBlockTracker(),\n };\n\n const install = (pi: PiExtensionApiLike, gate: ToolGateContext): void => {\n pi.on('tool_call', async (event) => {\n // A latched post-scan violation blocks everything that follows.\n if (state.criticalViolation) {\n return {\n block: true,\n reason: 'Run terminated by a security violation.',\n };\n }\n state.toolCalls += 1;\n if (state.toolCalls > MAX_TOOL_CALLS) {\n return {\n block: true,\n reason: `Stopped: exceeded ${MAX_TOOL_CALLS} tool calls (runaway guard).`,\n };\n }\n const decision = await evaluateToolCall(\n event.toolName,\n event.input ?? {},\n gate,\n llmProvider,\n );\n if (decision.block) {\n state.blockedCount += 1;\n if (decision.terminate) state.criticalViolation = true;\n logToFile(`[pi-security] BLOCK ${event.toolName}: ${decision.reason}`);\n return { block: true, reason: decision.reason };\n }\n // Keep .env files out of directory greps; a glob that selects one is refused above.\n if (event.toolName === 'grep' && event.input && !event.input.glob)\n event.input.glob = '!.env*';\n return {};\n });\n\n pi.on('tool_result', async (event) => {\n if (!isPostScanTool(event.toolName)) return {};\n const text = (event.content ?? [])\n .map((c) => (c && c.type === 'text' ? c.text : ''))\n .join('\\n');\n if (!text) return {};\n try {\n // 'input'-context rules (prompt injection in read content), with\n // triage — same policy as the anthropic PostToolUse Read hook.\n const matches = await scanAndTriage(text, 'input', llmProvider);\n const verdict = scanVerdict(matches);\n // Count the scan even when clean, like the anthropic hooks do.\n recordExternalScan(\n 'PostToolUse',\n event.toolName === 'read' ? 'Read' : 'Bash',\n matches.map(toReportViolation),\n verdict?.action ?? 'warned',\n );\n if (!verdict) return {};\n if (verdict.terminal) {\n state.criticalViolation = true;\n logToFile(\n `[pi-security] POST-SCAN VIOLATION ${event.toolName}: ${verdict.match.rule}`,\n );\n } else {\n logToFile(\n `[pi-security] POST-SCAN WARNING ${event.toolName}: ${\n verdict.match.rule\n } (severity: ${verdict.match.metadata.severity ?? 'unknown'})`,\n );\n }\n } catch (err) {\n // Fail closed: a scanner error on output latches a violation.\n state.criticalViolation = true;\n logToFile('[pi-security] post-scan error — failing closed:', err);\n }\n return {};\n });\n };\n\n const subagentCtx: ToolGateContext = {\n ...gateCtx,\n workingDirectory: undefined,\n };\n return {\n factory: (pi) => install(pi, gateCtx),\n subagentFactory: ((pi: PiExtensionApiLike) =>\n install(pi, subagentCtx)) as SubagentSecurityFactory,\n state,\n };\n}\n\n/**\n * Minimal structural type for pi's ExtensionAPI — just the `on` overloads we\n * use. Kept local so this module has no value import from the pi SDK (so the\n * CommonJS unit tests can load it directly). Handlers may return promises —\n * pi's real ExtensionHandler is `(event, ctx) => Promise<R | void> | R | void`.\n */\nexport interface PiExtensionApiLike {\n on(\n event: 'tool_call',\n handler: (event: {\n toolName: string;\n input?: Record<string, unknown>;\n }) =>\n | { block?: boolean; reason?: string }\n | Promise<{ block?: boolean; reason?: string }>,\n ): void;\n on(\n event: 'tool_result',\n handler: (event: {\n toolName: string;\n content?: Array<{ type: string; text?: string }>;\n isError?: boolean;\n }) => Record<string, never> | Promise<Record<string, never>>,\n ): void;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"security-Bk0aumrm.js","names":[],"sources":["../src/agent/runner/harness/pi/security.ts"],"sourcesContent":["/**\n * Fail-closed security for the pi backend (#525). pi has no built-in\n * permission layer, so we attach an extension that intercepts every tool call\n * — built-in (bash/read/edit/write/grep) AND custom — through pi's `tool_call`\n * hook and reuses the shared tool policy: `wizardCanUseTool` (the bash fence,\n * whose one `rm` rule is project-scoped, + .env fencing) plus the YARA\n * pre-scan. A `tool_result` hook\n * post-scans output. Both fail closed: a scanner error blocks, and a critical\n * post-scan violation latches so every subsequent tool call is blocked and the\n * run terminates as a YARA violation.\n *\n * Scanning runs through @posthog/warlock via yara-hooks' `scanAndTriage` — the\n * same engine, rules, chunking, and LLM triage policy as the anthropic\n * harness. pi handlers are async (pi's ExtensionHandler accepts promises), so\n * the WASM scan awaits inline.\n *\n * This is the one fence. Subagents run their own pi session with its subagent\n * gate installed (see subagent.ts): the same fence and state, with no `rm`.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport type { LLMProvider, ScanMatch } from '@posthog/warlock';\nimport { wizardCanUseTool } from '../../../agent-interface';\nimport {\n createRepeatBlockTracker,\n isWizardDocumentationPath,\n recordExternalScan,\n repeatBlockReason,\n scanAndTriage,\n type RepeatBlockTracker,\n} from '../../../yara-hooks';\nimport {\n publishBlockingMatch,\n scanVerdict,\n type ScanContext,\n} from '../../../yara-policy';\nimport { logToFile } from '@utils/debug';\nimport { analytics } from '@utils/analytics';\n\n/** warlock ScanMatch → the report shape `recordExternalScan` expects. */\nfunction toReportViolation(m: ScanMatch) {\n return {\n rule: m.rule,\n severity: m.metadata.severity ?? 'unknown',\n category: m.metadata.category ?? 'unknown',\n description: m.metadata.description ?? '',\n };\n}\n\n/** Runaway backstop: hard cap on tool calls per (sub)agent session. */\nexport const MAX_TOOL_CALLS = 250;\n\n/**\n * Passive telemetry for an unfixed upstream failure: gpt-5.x streaming with\n * tools leaks its function-call grammar into string values, and the leak lands\n * on disk (run 9704a73e shipped `…functions.complete_task (commentary…` plus a\n * DEL byte inside a customer's global-error.tsx). Observe-only — never blocks.\n * Prior art: https://github.com/BerriAI/litellm/issues/14260 (closed stale),\n * https://community.openai.com/t/1386422 (gpt-5.6-luna, no fix).\n */\nconst TRANSPORT_LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [\n // The litellm#14260 signature, scoped to the wizard's own tool names — a\n // generic `functions.*` would false-positive on real code (Firebase).\n {\n pattern: /functions\\.(?:complete_task|enqueue_task|read_handoffs)/,\n label: 'leaked tool-call tokens',\n },\n {\n pattern: /<\\|(?:channel|constrain|message|call|end|start|return)\\|>/,\n label: 'leaked channel markers',\n },\n // C0 controls and DEL minus tab/newline/CR — never valid in source text.\n // eslint-disable-next-line no-control-regex\n { pattern: /[\\0-\\x08\\x0B\\f\\x0E-\\x1F\\x7F]/, label: 'control characters' },\n];\n\n/** Capture one event per leaking write/edit: which pattern fired and where it sat — no matched text, ever. */\nexport function observeTransportLeak(tool: string, content: string): void {\n for (const { pattern, label } of TRANSPORT_LEAK_PATTERNS) {\n const match = pattern.exec(content);\n if (!match) continue;\n analytics.wizardCapture('file content leak observed', {\n tool,\n leak: label,\n leak_offset: match.index,\n content_length: content.length,\n // End-of-string leaks are the upstream decoder signature.\n at_end: content.length - match.index < 80,\n });\n logToFile(\n `[transport-leak] observed ${tool}: ${label} at ${match.index}/${content.length}`,\n );\n return;\n }\n}\n\nexport const READ_ONLY_PI_TOOLS: ReadonlySet<string> = new Set([\n 'read',\n 'grep',\n 'find',\n 'ls',\n]);\n\nexport interface ToolGateContext {\n readOnly?: boolean;\n disallowedTools?: readonly string[];\n /** True while a wizard_ask overlay is open (interactive); blocks Write/Edit. */\n getWizardAskPending?: () => boolean;\n /** Scan-triage classifier, resolved once in bootstrap. Absent → every flagged match is acted on. */\n triageProvider?: LLMProvider;\n /**\n * Per-run tracker of YARA-blocked payloads. When the agent retries content\n * that was already blocked, the block reason escalates (\"change the code,\n * not the retry\" → \"report it and move on\"). The extension wires one in;\n * absent (tests calling `evaluateToolCall` directly) every block reads as\n * a first attempt.\n */\n repeatTracker?: RepeatBlockTracker;\n /** Project root; rm is only allowed for files resolving strictly inside it. */\n workingDirectory?: string;\n}\n\nexport interface GateDecision {\n block: boolean;\n reason?: string;\n /** End the run, don't just refuse the call. Set when a blocked publish_handoff would otherwise leave no report. */\n terminate?: boolean;\n}\n\nconst str = (v: unknown): string => (typeof v === 'string' ? v : '');\n\n/** A pi tool path as pi opens it: its `resolveToCwd` strips a leading `@` and decodes `file://`. */\nfunction piToolPath(v: unknown): string {\n const raw = str(v);\n const p = raw.startsWith('@') ? raw.slice(1) : raw;\n if (!p.startsWith('file://')) return p;\n try {\n return fileURLToPath(p);\n } catch {\n return p; // pi fails to open it too\n }\n}\n\n// Shell metacharacters that chain, substitute, or redirect. A command carrying\n// any of these is more than one action, so we never treat it as a plain `rm`.\nconst SHELL_OPERATORS = /[;&|`$(){}<>\\n'\"\\\\]/;\n\n/** True when a target resolves to a file strictly inside the project root. */\nfunction isDeletableProjectFile(\n target: string,\n root: string,\n p: typeof path,\n): boolean {\n if (target.startsWith('-')) return false; // a flag, not a file\n if (/[*?[\\]~]/.test(target)) return false; // glob / home expansion\n if (p.basename(target).startsWith('.env')) return false; // secrets\n\n const canonicalRoot = p.resolve(root);\n const resolved = p.resolve(canonicalRoot, target);\n return (\n resolved !== canonicalRoot && resolved.startsWith(canonicalRoot + p.sep)\n );\n}\n\n/**\n * A plain `rm [-f] <file...>` whose every target is a file inside the project\n * root — the deletion shape the anthropic arm permits (there bash isn't\n * allowlisted; the shared YARA destructive-delete rules catch the dangerous\n * forms). pi rescues the same shape so the fall-through YARA scan can judge\n * it, but refuses any shell operator so it can't smuggle a second command.\n * Path checks run through the host's `path` (win32 on Windows, posix elsewhere);\n * pi always executes commands via a POSIX bash, so targets use forward slashes.\n */\nexport function isScopedFileRemoval(\n command: string,\n rawRoot: string | undefined,\n p: typeof path = path,\n): boolean {\n if (!rawRoot) return false; // no root to contain against → never rescue\n const root = p.resolve(rawRoot);\n const trimmed = command.trim();\n if (SHELL_OPERATORS.test(trimmed)) return false;\n\n const [executable, ...args] = trimmed.split(/\\s+/);\n if (executable !== 'rm') return false;\n\n if (args[0] === '-f') args.shift();\n if (args.length === 0) return false;\n\n return args.every((arg) => isDeletableProjectFile(arg, root, p));\n}\n\n/** A write that removes more than this fraction of an existing file's\n * non-whitespace content reads as a destructive whole-file rewrite. */\nconst OVERWRITE_SHRINK_LIMIT = 0.3;\n\n/** Existing files below this many non-whitespace chars are stubs; replacing one\n * wholesale is legitimate, so the shrink guard leaves them alone. */\nconst OVERWRITE_MIN_CHARS = 400;\n\nconst nonWhitespaceLength = (s: string): number => s.replace(/\\s+/g, '').length;\n\n/**\n * Refuse a write that replaces an existing file with far less content than it\n * held, steering the agent back to targeted edits. Prior art — the destructive\n * whole-file rewrite is a known model failure that targeted-edit formats exist to\n * prevent: Aider's search/replace and unified-diff, OpenAI's apply_patch,\n * Anthropic's str_replace with replace_all; pi's edit tool has no replace_all, so\n * a rejected scoped edit can fall back to a lossy full write. Additive by design:\n * the wizard only adds instrumentation, so a large shrink is the rewrite, not intent.\n */\nexport function overwriteShrinkReason(\n existing: string,\n next: string,\n): string | undefined {\n const before = nonWhitespaceLength(existing);\n if (before < OVERWRITE_MIN_CHARS) return undefined;\n const after = nonWhitespaceLength(next);\n if (after >= before * (1 - OVERWRITE_SHRINK_LIMIT)) return undefined;\n const removed = Math.round((1 - after / before) * 100);\n return `This write replaces an existing file and removes ~${removed}% of its content. Make targeted edits instead of rewriting the whole file; only write a file in full when creating it.`;\n}\n\n/**\n * Read the file a write would replace and flag a destructive shrink. A path that\n * does not exist yet is a new file, so it never triggers; with no project root to\n * resolve against (unit tests calling `evaluateToolCall` bare), it stays inert.\n */\nasync function overwriteShrinkBlock(\n input: Record<string, unknown>,\n workingDirectory: string | undefined,\n): Promise<string | undefined> {\n const target = piToolPath(input.path);\n if (!workingDirectory || !target) return undefined;\n let existing: string;\n try {\n existing = await fs.promises.readFile(\n path.resolve(workingDirectory, target),\n 'utf8',\n );\n } catch {\n return undefined; // new file (ENOENT) or unreadable — nothing to preserve\n }\n return overwriteShrinkReason(existing, str(input.content));\n}\n\n/**\n * Translate a pi tool name to the claude-cased name + input the shared policy\n * expects. pi field names (from the live tool stream): bash{command},\n * read/edit/write{path}, write adds {content}, edit adds {edits}, grep{path}.\n */\nfunction toClaudePolicyCall(\n toolName: string,\n input: Record<string, unknown>,\n): { name: string; input: Record<string, unknown> } {\n switch (toolName) {\n case 'bash':\n return { name: 'Bash', input: { command: str(input.command) } };\n case 'read':\n return { name: 'Read', input: { file_path: piToolPath(input.path) } };\n case 'write':\n return { name: 'Write', input: { file_path: piToolPath(input.path) } };\n case 'edit':\n return { name: 'Edit', input: { file_path: piToolPath(input.path) } };\n case 'grep':\n return {\n name: 'Grep',\n input: { path: piToolPath(input.path), glob: input.glob },\n };\n default:\n // Custom tools (load_skill_menu, set_env_values, dispatch_agent, …) +\n // find/ls: no path/command, policy allows (their own handlers are fenced).\n return { name: toolName, input };\n }\n}\n\n/**\n * The agent-facing block message. Includes the rule's remediation so a blocked\n * agent can self-correct (e.g. \"move PII to identify()/$set\") instead of\n * retrying near-identical content.\n */\nfunction blockMessage(m: ScanMatch): string {\n const remediation = m.metadata.remediation\n ? ` Fix: ${m.metadata.remediation}.`\n : '';\n return `[YARA] ${m.rule}: ${\n m.metadata.description ?? 'security rule matched'\n }.${remediation} Blocked for security.`;\n}\n\n/**\n * YARA scan of the content a tool is about to act on, BEFORE it executes.\n * - bash → scan the command ('command'-context rules: exfiltration,\n * destructive, force-push). No triage — a flagged command always blocks,\n * same as the anthropic PreToolUse hook, so the LLM call would be wasted.\n * - write/edit → scan the content being written ('output'-context rules:\n * hardcoded keys, PII), WITH triage, and with the same wizard-doc\n * `posthog_pii` suppression the anthropic path uses so the agent's own\n * event-plan files aren't blocked.\n * - publish_handoff → scan the report ('output'-context rules) with triage,\n * but only a CRITICAL match refuses the publish — it's the only channel.\n * Returns a block reason, or undefined to allow. Read/grep are post-scanned on\n * their output (in the tool_result hook), not here.\n */\nasync function preExecutionYaraBlock(\n toolName: string,\n input: Record<string, unknown>,\n llmProvider: LLMProvider | undefined,\n repeatTracker?: RepeatBlockTracker,\n): Promise<string | undefined> {\n let content: string;\n let ctx: ScanContext;\n let triage: LLMProvider | undefined;\n // Report labels for the scan report (phase/tool), matching the anthropic\n // hooks so pi and anthropic runs read the same in telemetry.\n let phase: string;\n let tool: string;\n switch (toolName) {\n case 'bash':\n content = str(input.command);\n ctx = 'command';\n triage = undefined;\n phase = 'PreToolUse';\n tool = 'Bash';\n break;\n case 'write':\n content = str(input.content);\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Write';\n break;\n case 'edit':\n // Scan ONLY the replacement text. `edits` is [{oldText, newText}] and\n // oldText is pre-existing file content the agent is changing — scanning\n // it blocks edits whose *surroundings* contain a violation, and even\n // edits that are removing one (field FPs: capture edits adjacent to\n // existing identify() PII; pre-existing violations blocking their own\n // replacement). Matches the anthropic path, which scans new_string only.\n content = (Array.isArray(input.edits) ? input.edits : [])\n .map((e) => str((e as Record<string, unknown>)?.newText))\n .join('\\n');\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Edit';\n break;\n case 'publish_handoff':\n // Agent-authored content persisted to the task stream and the host file.\n // Own labels, not Write's — the block bar differs (critical only).\n content = str(input.content);\n ctx = 'output';\n triage = llmProvider;\n phase = 'PreToolUse';\n tool = 'publish_handoff';\n break;\n default:\n return undefined;\n }\n if (!content) return undefined;\n if (ctx === 'output') observeTransportLeak(tool, content);\n\n let matches = await scanAndTriage(content, ctx, triage);\n if (ctx === 'output' && isWizardDocumentationPath(piToolPath(input.path))) {\n matches = matches.filter((m) => m.metadata.category !== 'posthog_pii');\n }\n // Any match blocks — except publish_handoff, critical only.\n const blocking =\n toolName === 'publish_handoff'\n ? publishBlockingMatch(matches)\n : matches[0] ?? null;\n\n recordExternalScan(\n phase,\n tool,\n matches.map(toReportViolation),\n blocking ? 'blocked' : 'warned',\n );\n if (!blocking) return undefined;\n\n const attempt = repeatTracker?.attempt(toolName, content) ?? 1;\n return repeatBlockReason(attempt, toolName, blockMessage(blocking));\n}\n\n/**\n * The pure gate decision for a single tool call. Reuses `wizardCanUseTool`\n * (deny → block) then the YARA content scan (match → block). Fail-closed: any\n * thrown error blocks.\n */\nexport async function evaluateToolCall(\n toolName: string,\n input: Record<string, unknown>,\n ctx: ToolGateContext = {},\n llmProvider?: LLMProvider,\n): Promise<GateDecision> {\n try {\n if (ctx.readOnly && !READ_ONLY_PI_TOOLS.has(toolName)) {\n return {\n block: true,\n reason: `Tool ${toolName} is disabled for read-only runs.`,\n };\n }\n const policy = toClaudePolicyCall(toolName, input);\n const decision = wizardCanUseTool(policy.name, policy.input, {\n disallowedTools: ctx.disallowedTools,\n wizardAskPending: ctx.getWizardAskPending?.() ?? false,\n workingDirectory: ctx.workingDirectory,\n });\n if (decision.behavior === 'deny') {\n return { block: true, reason: decision.message };\n }\n\n const yaraReason = await preExecutionYaraBlock(\n toolName,\n input,\n llmProvider,\n ctx.repeatTracker,\n );\n if (yaraReason) return { block: true, reason: yaraReason };\n\n if (toolName === 'write') {\n const shrinkReason = await overwriteShrinkBlock(\n input,\n ctx.workingDirectory,\n );\n if (shrinkReason) return { block: true, reason: shrinkReason };\n }\n\n return { block: false };\n } catch (err) {\n logToFile('[pi-security] gate error — failing closed:', err);\n // A publish_handoff the gate can't clear leaves the run with no report at\n // all, so end it visibly instead of letting the agent reword and retry.\n return {\n block: true,\n reason: 'Security check failed; tool blocked (fail-closed).',\n terminate: toolName === 'publish_handoff',\n };\n }\n}\n\n/** pi result tool names whose OUTPUT gets scanned for 'input'-context rules. */\nfunction isPostScanTool(toolName: string): boolean {\n return toolName === 'read' || toolName === 'bash';\n}\n\n/** Mutable state the backend reads after the run to classify the outcome. */\nexport interface SecurityState {\n criticalViolation: boolean;\n blockedCount: number;\n toolCalls: number;\n}\n\n/** Options for {@link createSecurityExtension}. The root is required, so no run loses the rm allowance or the shrink guard by omission. */\nexport type SecurityExtensionOptions = ToolGateContext & {\n workingDirectory: string;\n};\n\ndeclare const subagentGate: unique symbol;\n\n/** A gate with no project root, so it never allows rm. Only `subagentFactory` makes one, so a subagent can't be handed the parent's. */\nexport type SubagentSecurityFactory = ((pi: PiExtensionApiLike) => void) & {\n readonly [subagentGate]: true;\n};\n\n/**\n * Build the pi security extension + the shared state the backend inspects.\n * Install `factory` via `extensionFactories`. Give subagent sessions\n * `subagentFactory`: the same fence and state, with no project root, so no rm.\n */\nexport function createSecurityExtension(ctx: SecurityExtensionOptions): {\n factory: (pi: PiExtensionApiLike) => void;\n subagentFactory: SubagentSecurityFactory;\n state: SecurityState;\n} {\n const state: SecurityState = {\n criticalViolation: false,\n blockedCount: 0,\n toolCalls: 0,\n };\n\n // One triage provider per extension. Undefined (no gateway auth) means\n // triage is skipped and every flagged match is acted on — fail closed.\n const llmProvider = ctx.triageProvider;\n\n // One tracker per extension = per (sub)agent session, so an identical\n // payload retried after a block gets the escalating reason.\n const gateCtx: ToolGateContext = {\n ...ctx,\n repeatTracker: ctx.repeatTracker ?? createRepeatBlockTracker(),\n };\n\n const install = (pi: PiExtensionApiLike, gate: ToolGateContext): void => {\n pi.on('tool_call', async (event) => {\n // A latched post-scan violation blocks everything that follows.\n if (state.criticalViolation) {\n return {\n block: true,\n reason: 'Run terminated by a security violation.',\n };\n }\n state.toolCalls += 1;\n if (state.toolCalls > MAX_TOOL_CALLS) {\n return {\n block: true,\n reason: `Stopped: exceeded ${MAX_TOOL_CALLS} tool calls (runaway guard).`,\n };\n }\n const decision = await evaluateToolCall(\n event.toolName,\n event.input ?? {},\n gate,\n llmProvider,\n );\n if (decision.block) {\n state.blockedCount += 1;\n if (decision.terminate) state.criticalViolation = true;\n logToFile(`[pi-security] BLOCK ${event.toolName}: ${decision.reason}`);\n return { block: true, reason: decision.reason };\n }\n // Keep .env files out of directory greps; a glob that selects one is refused above.\n if (event.toolName === 'grep' && event.input && !event.input.glob)\n event.input.glob = '!.env*';\n return {};\n });\n\n pi.on('tool_result', async (event) => {\n if (!isPostScanTool(event.toolName)) return {};\n const text = (event.content ?? [])\n .map((c) => (c && c.type === 'text' ? c.text : ''))\n .join('\\n');\n if (!text) return {};\n try {\n // 'input'-context rules (prompt injection in read content), with\n // triage — same policy as the anthropic PostToolUse Read hook.\n const matches = await scanAndTriage(text, 'input', llmProvider);\n const verdict = scanVerdict(matches);\n // Count the scan even when clean, like the anthropic hooks do.\n recordExternalScan(\n 'PostToolUse',\n event.toolName === 'read' ? 'Read' : 'Bash',\n matches.map(toReportViolation),\n verdict?.action ?? 'warned',\n );\n if (!verdict) return {};\n if (verdict.terminal) {\n state.criticalViolation = true;\n logToFile(\n `[pi-security] POST-SCAN VIOLATION ${event.toolName}: ${verdict.match.rule}`,\n );\n } else {\n logToFile(\n `[pi-security] POST-SCAN WARNING ${event.toolName}: ${\n verdict.match.rule\n } (severity: ${verdict.match.metadata.severity ?? 'unknown'})`,\n );\n }\n } catch (err) {\n // Fail closed: a scanner error on output latches a violation.\n state.criticalViolation = true;\n logToFile('[pi-security] post-scan error — failing closed:', err);\n }\n return {};\n });\n };\n\n const subagentCtx: ToolGateContext = {\n ...gateCtx,\n workingDirectory: undefined,\n };\n return {\n factory: (pi) => install(pi, gateCtx),\n subagentFactory: ((pi: PiExtensionApiLike) =>\n install(pi, subagentCtx)) as SubagentSecurityFactory,\n state,\n };\n}\n\n/**\n * Minimal structural type for pi's ExtensionAPI — just the `on` overloads we\n * use. Kept local so this module has no value import from the pi SDK (so the\n * CommonJS unit tests can load it directly). Handlers may return promises —\n * pi's real ExtensionHandler is `(event, ctx) => Promise<R | void> | R | void`.\n */\nexport interface PiExtensionApiLike {\n on(\n event: 'tool_call',\n handler: (event: {\n toolName: string;\n input?: Record<string, unknown>;\n }) =>\n | { block?: boolean; reason?: string }\n | Promise<{ block?: boolean; reason?: string }>,\n ): void;\n on(\n event: 'tool_result',\n handler: (event: {\n toolName: string;\n content?: Array<{ type: string; text?: string }>;\n isError?: boolean;\n }) => Record<string, never> | Promise<Record<string, never>>,\n ): void;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import "./analytics-CX2kvq0R.js";
2
- import "./package-manager-Zw0JN1Kh.js";
1
+ import "./analytics-CZNcNpiq.js";
2
+ import "./package-manager-CPzIaeu0.js";
3
3
  import { major, minVersion } from "semver";
4
4
  import { basename, join } from "node:path";
5
5
  import * as childProcess from "node:child_process";
@@ -115,4 +115,4 @@ function isUsingTypeScript({ installDir }) {
115
115
  //#endregion
116
116
  export { createVersionBucket as i, isUsingTypeScript as n, tryGetPackageJson as r, detectOrgAndProject as t };
117
117
 
118
- //# sourceMappingURL=setup-utils-DBOw3lnu.js.map
118
+ //# sourceMappingURL=setup-utils-D5SquGvT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup-utils-DBOw3lnu.js","names":[],"sources":["../src/shared/utils/semver.ts","../src/shared/utils/setup-utils.ts"],"sourcesContent":["import {\n major,\n minVersion,\n satisfies,\n subset,\n valid,\n validRange,\n} from 'semver';\n\n/**\n * Version strings from package.json that are not semver ranges.\n * URLs, git refs, dist-tags, local paths, workspace protocol, npm aliases, etc.\n * These should be rejected early — we can't determine a clear version from them.\n */\nfunction isNonSemverVersion(version: string): boolean {\n const v = version.trim();\n return (\n v === '' ||\n v.startsWith('http://') ||\n v.startsWith('https://') ||\n v.startsWith('git+') ||\n v.startsWith('git://') ||\n v.startsWith('file:') ||\n v.startsWith('npm:') ||\n v.startsWith('workspace:') ||\n v.startsWith('/') ||\n v.includes('/') // user/repo shorthand\n );\n}\n\nexport function versionSatisfiesRange({\n version,\n acceptableVersions,\n canBeLatest,\n}: {\n version: string;\n acceptableVersions: string;\n canBeLatest: boolean;\n}): boolean {\n if (version === 'latest') return canBeLatest;\n if (isNonSemverVersion(version)) return false;\n\n const concrete = valid(version);\n if (concrete !== null) {\n return satisfies(concrete, acceptableVersions);\n }\n\n const userRange = validRange(version);\n if (userRange === null) return false;\n return subset(userRange, acceptableVersions);\n}\n\n/**\n * Creates a version bucket function for analytics.\n * Converts versions like \"1.2.3\" to \"1.x\" for grouping in analytics.\n *\n * @param minMajorVersion - Optional minimum major version threshold.\n * Versions below this will be bucketed as \"<{min}.0.0\"\n *\n * @example\n * const getVersionBucket = createVersionBucket(); // no minimum\n * getVersionBucket(\"1.2.3\") // \"1.x\"\n *\n * const getVersionBucket = createVersionBucket(11);\n * getVersionBucket(\"15.3.0\") // \"15.x\"\n * getVersionBucket(\"10.0.0\") // \"<11.0.0\"\n */\nexport function createVersionBucket(minMajorVersion?: number) {\n return (version: string | undefined): string => {\n if (!version) {\n return 'none';\n }\n\n if (isNonSemverVersion(version)) {\n return 'unknown';\n }\n\n try {\n const minVer = minVersion(version);\n if (!minVer) {\n return 'invalid';\n }\n const majorVersion = major(minVer);\n if (minMajorVersion !== undefined && majorVersion < minMajorVersion) {\n return `<${minMajorVersion}.0.0`;\n }\n return `${majorVersion}.x`;\n } catch {\n return 'unknown';\n }\n };\n}\n","import * as childProcess from 'node:child_process';\nimport * as fs from 'node:fs';\nimport { basename, join } from 'node:path';\nimport {\n type PackageManager,\n detectAllPackageManagers,\n NPM as npm,\n} from './package-manager';\nimport type { WizardRunOptions } from './types';\nimport type { PackageJson } from './package-json';\nimport { getDeclaredVersion } from './package-json';\nimport { analytics } from './analytics';\nimport { versionSatisfiesRange } from './semver';\n\nexport interface CliSetupConfig {\n filename: string;\n name: string;\n gitignore: boolean;\n\n likelyAlreadyHasAuthToken(contents: string): boolean;\n tokenContent(authToken: string): string;\n\n likelyAlreadyHasOrgAndProject(contents: string): boolean;\n orgAndProjContent(org: string, project: string): string;\n\n likelyAlreadyHasUrl?(contents: string): boolean;\n urlContent?(url: string): string;\n}\n\nexport interface CliSetupConfigContent {\n authToken: string;\n org?: string;\n project?: string;\n url?: string;\n}\n\nconst FREEMAIL_DOMAINS = new Set([\n 'gmail.com',\n 'googlemail.com',\n 'hotmail.com',\n 'outlook.com',\n 'yahoo.com',\n 'icloud.com',\n 'me.com',\n 'mail.com',\n 'protonmail.com',\n 'proton.me',\n 'live.com',\n 'aol.com',\n 'yandex.com',\n 'zoho.com',\n 'gmx.com',\n 'fastmail.com',\n]);\n\nfunction parseGitRemote(): { org: string; repo: string } | null {\n try {\n const url = childProcess\n .execSync('git remote get-url origin', {\n stdio: ['ignore', 'pipe', 'ignore'],\n })\n .toString()\n .trim();\n // git@github.com:acme-corp/my-app.git or https://github.com/acme-corp/my-app.git\n const match = url.match(/[/:]([\\w.-]+)\\/([\\w.-]+?)(?:\\.git)?$/);\n if (match) return { org: match[1], repo: match[2] };\n } catch {\n // not in a git repo or no remote\n }\n return null;\n}\n\nexport function detectOrgAndProject(email: string): {\n orgName: string | undefined;\n projectName: string | undefined;\n} {\n const remote = parseGitRemote();\n\n // Project name: git repo name > directory name\n const projectName = remote?.repo || basename(process.cwd()) || undefined;\n\n // Org name: git remote org > email domain (skip freemail)\n let orgName: string | undefined;\n if (remote?.org) {\n orgName = remote.org;\n } else {\n const domain = email.split('@')[1]?.toLowerCase();\n if (domain && !FREEMAIL_DOMAINS.has(domain)) {\n orgName = domain.split('.')[0];\n }\n }\n\n return { orgName, projectName };\n}\n\nexport async function isReact19Installed({\n installDir,\n}: Pick<WizardRunOptions, 'installDir'>): Promise<boolean> {\n try {\n const packageJson = await tryGetPackageJson({ installDir });\n if (!packageJson) return false;\n const reactVersion = getDeclaredVersion('react', packageJson);\n\n if (!reactVersion) {\n return false;\n }\n\n return versionSatisfiesRange({\n version: reactVersion,\n acceptableVersions: '>=19.0.0',\n canBeLatest: true,\n });\n } catch {\n return false;\n }\n}\n\n/**\n * Try to get package.json, returning null if it doesn't exist.\n * Use this for detection purposes where missing package.json is expected (e.g., Python projects).\n */\nexport async function tryGetPackageJson({\n installDir,\n}: Pick<WizardRunOptions, 'installDir'>): Promise<PackageJson | null> {\n try {\n const packageJsonFileContents = await fs.promises.readFile(\n join(installDir, 'package.json'),\n 'utf8',\n );\n return JSON.parse(packageJsonFileContents) as PackageJson;\n } catch {\n return null;\n }\n}\n\n/**\n * Detect and return the package manager. Pure — no prompts.\n * Falls back to first detected or npm if ambiguous.\n */\n// eslint-disable-next-line @typescript-eslint/require-await\nexport async function getPackageManager(\n options: Pick<WizardRunOptions, 'installDir'> & { ci?: boolean },\n): Promise<PackageManager> {\n const detectedPackageManagers = detectAllPackageManagers({\n installDir: options.installDir,\n });\n\n if (detectedPackageManagers.length >= 1) {\n const selected = detectedPackageManagers[0];\n analytics.setTag('package-manager', selected.name);\n return selected;\n }\n\n // No package manager detected — default to npm\n analytics.setTag('package-manager', npm.name);\n return npm;\n}\n\nexport function isUsingTypeScript({\n installDir,\n}: Pick<WizardRunOptions, 'installDir'>): boolean {\n try {\n fs.accessSync(join(installDir, 'tsconfig.json'));\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAS,mBAAmB,SAA0B;CACpD,MAAM,IAAI,QAAQ,MAAM;AACxB,QACE,MAAM,MACN,EAAE,WAAW,UAAU,IACvB,EAAE,WAAW,WAAW,IACxB,EAAE,WAAW,OAAO,IACpB,EAAE,WAAW,SAAS,IACtB,EAAE,WAAW,QAAQ,IACrB,EAAE,WAAW,OAAO,IACpB,EAAE,WAAW,aAAa,IAC1B,EAAE,WAAW,IAAI,IACjB,EAAE,SAAS,IAAI;;;;;;;;;;;;;;;;;AAyCnB,SAAgB,oBAAoB,iBAA0B;AAC5D,SAAQ,YAAwC;AAC9C,MAAI,CAAC,QACH,QAAO;AAGT,MAAI,mBAAmB,QAAQ,CAC7B,QAAO;AAGT,MAAI;GACF,MAAM,SAAS,WAAW,QAAQ;AAClC,OAAI,CAAC,OACH,QAAO;GAET,MAAM,eAAe,MAAM,OAAO;AAClC,OAAI,oBAAoB,KAAA,KAAa,eAAe,gBAClD,QAAO,IAAI,gBAAgB;AAE7B,UAAO,GAAG,aAAa;UACjB;AACN,UAAO;;;;;;ACpDb,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,iBAAuD;AAC9D,KAAI;EAQF,MAAM,QAPM,aACT,SAAS,6BAA6B,EACrC,OAAO;GAAC;GAAU;GAAQ;GAAS,EACpC,CAAC,CACD,UAAU,CACV,MAAM,CAES,MAAM,uCAAuC;AAC/D,MAAI,MAAO,QAAO;GAAE,KAAK,MAAM;GAAI,MAAM,MAAM;GAAI;SAC7C;AAGR,QAAO;;AAGT,SAAgB,oBAAoB,OAGlC;CACA,MAAM,SAAS,gBAAgB;CAG/B,MAAM,cAAc,QAAQ,QAAQ,SAAS,QAAQ,KAAK,CAAC,IAAI,KAAA;CAG/D,IAAI;AACJ,KAAI,QAAQ,IACV,WAAU,OAAO;MACZ;EACL,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,IAAI,aAAa;AACjD,MAAI,UAAU,CAAC,iBAAiB,IAAI,OAAO,CACzC,WAAU,OAAO,MAAM,IAAI,CAAC;;AAIhC,QAAO;EAAE;EAAS;EAAa;;;;;;AA6BjC,eAAsB,kBAAkB,EACtC,cACoE;AACpE,KAAI;EACF,MAAM,0BAA0B,MAAM,GAAG,SAAS,SAChD,KAAK,YAAY,eAAe,EAChC,OACD;AACD,SAAO,KAAK,MAAM,wBAAwB;SACpC;AACN,SAAO;;;AA2BX,SAAgB,kBAAkB,EAChC,cACgD;AAChD,KAAI;AACF,KAAG,WAAW,KAAK,YAAY,gBAAgB,CAAC;AAChD,SAAO;SACD;AACN,SAAO"}
1
+ {"version":3,"file":"setup-utils-D5SquGvT.js","names":[],"sources":["../src/shared/utils/semver.ts","../src/shared/utils/setup-utils.ts"],"sourcesContent":["import {\n major,\n minVersion,\n satisfies,\n subset,\n valid,\n validRange,\n} from 'semver';\n\n/**\n * Version strings from package.json that are not semver ranges.\n * URLs, git refs, dist-tags, local paths, workspace protocol, npm aliases, etc.\n * These should be rejected early — we can't determine a clear version from them.\n */\nfunction isNonSemverVersion(version: string): boolean {\n const v = version.trim();\n return (\n v === '' ||\n v.startsWith('http://') ||\n v.startsWith('https://') ||\n v.startsWith('git+') ||\n v.startsWith('git://') ||\n v.startsWith('file:') ||\n v.startsWith('npm:') ||\n v.startsWith('workspace:') ||\n v.startsWith('/') ||\n v.includes('/') // user/repo shorthand\n );\n}\n\nexport function versionSatisfiesRange({\n version,\n acceptableVersions,\n canBeLatest,\n}: {\n version: string;\n acceptableVersions: string;\n canBeLatest: boolean;\n}): boolean {\n if (version === 'latest') return canBeLatest;\n if (isNonSemverVersion(version)) return false;\n\n const concrete = valid(version);\n if (concrete !== null) {\n return satisfies(concrete, acceptableVersions);\n }\n\n const userRange = validRange(version);\n if (userRange === null) return false;\n return subset(userRange, acceptableVersions);\n}\n\n/**\n * Creates a version bucket function for analytics.\n * Converts versions like \"1.2.3\" to \"1.x\" for grouping in analytics.\n *\n * @param minMajorVersion - Optional minimum major version threshold.\n * Versions below this will be bucketed as \"<{min}.0.0\"\n *\n * @example\n * const getVersionBucket = createVersionBucket(); // no minimum\n * getVersionBucket(\"1.2.3\") // \"1.x\"\n *\n * const getVersionBucket = createVersionBucket(11);\n * getVersionBucket(\"15.3.0\") // \"15.x\"\n * getVersionBucket(\"10.0.0\") // \"<11.0.0\"\n */\nexport function createVersionBucket(minMajorVersion?: number) {\n return (version: string | undefined): string => {\n if (!version) {\n return 'none';\n }\n\n if (isNonSemverVersion(version)) {\n return 'unknown';\n }\n\n try {\n const minVer = minVersion(version);\n if (!minVer) {\n return 'invalid';\n }\n const majorVersion = major(minVer);\n if (minMajorVersion !== undefined && majorVersion < minMajorVersion) {\n return `<${minMajorVersion}.0.0`;\n }\n return `${majorVersion}.x`;\n } catch {\n return 'unknown';\n }\n };\n}\n","import * as childProcess from 'node:child_process';\nimport * as fs from 'node:fs';\nimport { basename, join } from 'node:path';\nimport {\n type PackageManager,\n detectAllPackageManagers,\n NPM as npm,\n} from './package-manager';\nimport type { WizardRunOptions } from './types';\nimport type { PackageJson } from './package-json';\nimport { getDeclaredVersion } from './package-json';\nimport { analytics } from './analytics';\nimport { versionSatisfiesRange } from './semver';\n\nexport interface CliSetupConfig {\n filename: string;\n name: string;\n gitignore: boolean;\n\n likelyAlreadyHasAuthToken(contents: string): boolean;\n tokenContent(authToken: string): string;\n\n likelyAlreadyHasOrgAndProject(contents: string): boolean;\n orgAndProjContent(org: string, project: string): string;\n\n likelyAlreadyHasUrl?(contents: string): boolean;\n urlContent?(url: string): string;\n}\n\nexport interface CliSetupConfigContent {\n authToken: string;\n org?: string;\n project?: string;\n url?: string;\n}\n\nconst FREEMAIL_DOMAINS = new Set([\n 'gmail.com',\n 'googlemail.com',\n 'hotmail.com',\n 'outlook.com',\n 'yahoo.com',\n 'icloud.com',\n 'me.com',\n 'mail.com',\n 'protonmail.com',\n 'proton.me',\n 'live.com',\n 'aol.com',\n 'yandex.com',\n 'zoho.com',\n 'gmx.com',\n 'fastmail.com',\n]);\n\nfunction parseGitRemote(): { org: string; repo: string } | null {\n try {\n const url = childProcess\n .execSync('git remote get-url origin', {\n stdio: ['ignore', 'pipe', 'ignore'],\n })\n .toString()\n .trim();\n // git@github.com:acme-corp/my-app.git or https://github.com/acme-corp/my-app.git\n const match = url.match(/[/:]([\\w.-]+)\\/([\\w.-]+?)(?:\\.git)?$/);\n if (match) return { org: match[1], repo: match[2] };\n } catch {\n // not in a git repo or no remote\n }\n return null;\n}\n\nexport function detectOrgAndProject(email: string): {\n orgName: string | undefined;\n projectName: string | undefined;\n} {\n const remote = parseGitRemote();\n\n // Project name: git repo name > directory name\n const projectName = remote?.repo || basename(process.cwd()) || undefined;\n\n // Org name: git remote org > email domain (skip freemail)\n let orgName: string | undefined;\n if (remote?.org) {\n orgName = remote.org;\n } else {\n const domain = email.split('@')[1]?.toLowerCase();\n if (domain && !FREEMAIL_DOMAINS.has(domain)) {\n orgName = domain.split('.')[0];\n }\n }\n\n return { orgName, projectName };\n}\n\nexport async function isReact19Installed({\n installDir,\n}: Pick<WizardRunOptions, 'installDir'>): Promise<boolean> {\n try {\n const packageJson = await tryGetPackageJson({ installDir });\n if (!packageJson) return false;\n const reactVersion = getDeclaredVersion('react', packageJson);\n\n if (!reactVersion) {\n return false;\n }\n\n return versionSatisfiesRange({\n version: reactVersion,\n acceptableVersions: '>=19.0.0',\n canBeLatest: true,\n });\n } catch {\n return false;\n }\n}\n\n/**\n * Try to get package.json, returning null if it doesn't exist.\n * Use this for detection purposes where missing package.json is expected (e.g., Python projects).\n */\nexport async function tryGetPackageJson({\n installDir,\n}: Pick<WizardRunOptions, 'installDir'>): Promise<PackageJson | null> {\n try {\n const packageJsonFileContents = await fs.promises.readFile(\n join(installDir, 'package.json'),\n 'utf8',\n );\n return JSON.parse(packageJsonFileContents) as PackageJson;\n } catch {\n return null;\n }\n}\n\n/**\n * Detect and return the package manager. Pure — no prompts.\n * Falls back to first detected or npm if ambiguous.\n */\n// eslint-disable-next-line @typescript-eslint/require-await\nexport async function getPackageManager(\n options: Pick<WizardRunOptions, 'installDir'> & { ci?: boolean },\n): Promise<PackageManager> {\n const detectedPackageManagers = detectAllPackageManagers({\n installDir: options.installDir,\n });\n\n if (detectedPackageManagers.length >= 1) {\n const selected = detectedPackageManagers[0];\n analytics.setTag('package-manager', selected.name);\n return selected;\n }\n\n // No package manager detected — default to npm\n analytics.setTag('package-manager', npm.name);\n return npm;\n}\n\nexport function isUsingTypeScript({\n installDir,\n}: Pick<WizardRunOptions, 'installDir'>): boolean {\n try {\n fs.accessSync(join(installDir, 'tsconfig.json'));\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAS,mBAAmB,SAA0B;CACpD,MAAM,IAAI,QAAQ,MAAM;AACxB,QACE,MAAM,MACN,EAAE,WAAW,UAAU,IACvB,EAAE,WAAW,WAAW,IACxB,EAAE,WAAW,OAAO,IACpB,EAAE,WAAW,SAAS,IACtB,EAAE,WAAW,QAAQ,IACrB,EAAE,WAAW,OAAO,IACpB,EAAE,WAAW,aAAa,IAC1B,EAAE,WAAW,IAAI,IACjB,EAAE,SAAS,IAAI;;;;;;;;;;;;;;;;;AAyCnB,SAAgB,oBAAoB,iBAA0B;AAC5D,SAAQ,YAAwC;AAC9C,MAAI,CAAC,QACH,QAAO;AAGT,MAAI,mBAAmB,QAAQ,CAC7B,QAAO;AAGT,MAAI;GACF,MAAM,SAAS,WAAW,QAAQ;AAClC,OAAI,CAAC,OACH,QAAO;GAET,MAAM,eAAe,MAAM,OAAO;AAClC,OAAI,oBAAoB,KAAA,KAAa,eAAe,gBAClD,QAAO,IAAI,gBAAgB;AAE7B,UAAO,GAAG,aAAa;UACjB;AACN,UAAO;;;;;;ACpDb,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,iBAAuD;AAC9D,KAAI;EAQF,MAAM,QAPM,aACT,SAAS,6BAA6B,EACrC,OAAO;GAAC;GAAU;GAAQ;GAAS,EACpC,CAAC,CACD,UAAU,CACV,MAAM,CAES,MAAM,uCAAuC;AAC/D,MAAI,MAAO,QAAO;GAAE,KAAK,MAAM;GAAI,MAAM,MAAM;GAAI;SAC7C;AAGR,QAAO;;AAGT,SAAgB,oBAAoB,OAGlC;CACA,MAAM,SAAS,gBAAgB;CAG/B,MAAM,cAAc,QAAQ,QAAQ,SAAS,QAAQ,KAAK,CAAC,IAAI,KAAA;CAG/D,IAAI;AACJ,KAAI,QAAQ,IACV,WAAU,OAAO;MACZ;EACL,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,IAAI,aAAa;AACjD,MAAI,UAAU,CAAC,iBAAiB,IAAI,OAAO,CACzC,WAAU,OAAO,MAAM,IAAI,CAAC;;AAIhC,QAAO;EAAE;EAAS;EAAa;;;;;;AA6BjC,eAAsB,kBAAkB,EACtC,cACoE;AACpE,KAAI;EACF,MAAM,0BAA0B,MAAM,GAAG,SAAS,SAChD,KAAK,YAAY,eAAe,EAChC,OACD;AACD,SAAO,KAAK,MAAM,wBAAwB;SACpC;AACN,SAAO;;;AA2BX,SAAgB,kBAAkB,EAChC,cACgD;AAChD,KAAI;AACF,KAAG,WAAW,KAAK,YAAY,gBAAgB,CAAC;AAChD,SAAO;SACD;AACN,SAAO"}
@@ -1,10 +1,10 @@
1
- import { c as WIZARD_LOG_FILE } from "./debug-C_9xDg9j.js";
2
- import "./analytics-CX2kvq0R.js";
3
- import { T as PROGRAM_REGISTRY, ut as getBlockingServiceKeys } from "./programs-7aIrstFy.js";
4
- import { t as HostResolution } from "./host-resolution-C3ctTKNg.js";
5
- import "./results-8xNiBbaP.js";
6
- import { f as ConfirmButton, h as Icons, m as Colors, t as PickerMenu } from "./PickerMenu-BtheKi1o.js";
7
- import { A as AUDIT_AREA_SLIDES, C as assembleProfile, D as getTuiProgram, E as IssueTable, F as ContentSequencer, G as ModalOverlay, H as LogViewer, I as HNViewer, J as ProgressList, K as ConfirmationInput, L as TabContainer, Q as getContentBlocks, R as ScreenContainer, S as McpSuggestedPromptsScreen, T as McpScreen, V as EventPlanViewer, X as SplitView, Y as LoadingBox, Z as CardLayout, a as SlackConnectScreen, c as LearnCard, i as OutroScreen, j as AuditChecksViewer, l as AiOptInRequiredScreen, n as releaseTerminal, o as TipsCard, q as GroupedPickerMenu, r as WizardStore, s as VisualizerTab, t as enterDarkTerminal, u as ServiceHealthList, v as seededProfile, w as TAILORED_ROLES, z as ViewportTooSmall } from "./terminal-B4EXuZra.js";
1
+ import { c as WIZARD_LOG_FILE } from "./debug-Bw13gmS3.js";
2
+ import "./analytics-CZNcNpiq.js";
3
+ import { T as PROGRAM_REGISTRY, ut as getBlockingServiceKeys } from "./programs-T4EhU20m.js";
4
+ import { t as HostResolution } from "./host-resolution-B0cQUK2A.js";
5
+ import "./results-CXTeEIUi.js";
6
+ import { f as ConfirmButton, h as Icons, m as Colors, t as PickerMenu } from "./PickerMenu-DB8MO29j.js";
7
+ import { A as AUDIT_AREA_SLIDES, C as assembleProfile, D as getTuiProgram, E as IssueTable, F as ContentSequencer, G as ModalOverlay, H as LogViewer, I as HNViewer, J as ProgressList, K as ConfirmationInput, L as TabContainer, Q as getContentBlocks, R as ScreenContainer, S as McpSuggestedPromptsScreen, T as McpScreen, V as EventPlanViewer, X as SplitView, Y as LoadingBox, Z as CardLayout, a as SlackConnectScreen, c as LearnCard, i as OutroScreen, j as AuditChecksViewer, l as AiOptInRequiredScreen, n as releaseTerminal, o as TipsCard, q as GroupedPickerMenu, r as WizardStore, s as VisualizerTab, t as enterDarkTerminal, u as ServiceHealthList, v as seededProfile, w as TAILORED_ROLES, z as ViewportTooSmall } from "./terminal-BLGYQpSO.js";
8
8
  import * as fs$1 from "fs";
9
9
  import * as path$1 from "path";
10
10
  import * as os from "os";
@@ -919,21 +919,25 @@ const ModalDemo = () => {
919
919
  const MOCK_CLIENTS = [
920
920
  {
921
921
  name: "Claude Code",
922
+ kind: "local",
922
923
  supportsPlugin: true,
923
924
  pluginBundlesMcp: false
924
925
  },
925
926
  {
926
927
  name: "Cursor",
928
+ kind: "local",
927
929
  supportsPlugin: true,
928
930
  pluginBundlesMcp: false
929
931
  },
930
932
  {
931
933
  name: "VS Code",
934
+ kind: "local",
932
935
  supportsPlugin: false,
933
936
  pluginBundlesMcp: false
934
937
  },
935
938
  {
936
939
  name: "Claude Desktop/Web",
940
+ kind: "account",
937
941
  supportsPlugin: false,
938
942
  pluginBundlesMcp: false,
939
943
  finish: {
@@ -949,11 +953,18 @@ function createMockInstaller() {
949
953
  return MOCK_CLIENTS;
950
954
  },
951
955
  async install(clientNames) {
952
- await new Promise((r) => setTimeout(r, 1500));
953
- return clientNames.map((name, i) => ({
956
+ await new Promise((r) => setTimeout(r, 700));
957
+ return clientNames.map((name) => name === "Cursor" ? {
954
958
  name,
955
- status: i === 1 ? "unchanged" : "changed"
956
- }));
959
+ status: "unchanged"
960
+ } : name === "VS Code" ? {
961
+ name,
962
+ status: "failed",
963
+ detail: "EACCES: permission denied"
964
+ } : {
965
+ name,
966
+ status: "changed"
967
+ });
957
968
  },
958
969
  async installPlugins(clientNames) {
959
970
  await new Promise((r) => setTimeout(r, 800));
@@ -962,9 +973,13 @@ function createMockInstaller() {
962
973
  status: "changed"
963
974
  }));
964
975
  },
965
- async remove() {
976
+ async detectInstalled() {
977
+ await new Promise((r) => setTimeout(r, 800));
978
+ return MOCK_CLIENTS.filter((c) => c.kind === "local");
979
+ },
980
+ async remove(clientNames) {
966
981
  await new Promise((r) => setTimeout(r, 1e3));
967
- return MOCK_CLIENTS.map((c, i) => i === 2 ? {
982
+ return MOCK_CLIENTS.filter((c) => clientNames.includes(c.name)).map((c) => c.name === "VS Code" ? {
968
983
  name: c.name,
969
984
  status: "failed",
970
985
  detail: "EACCES: permission denied"
@@ -2516,4 +2531,4 @@ function startPlayground(version) {
2516
2531
  //#endregion
2517
2532
  export { startPlayground };
2518
2533
 
2519
- //# sourceMappingURL=start-playground-Uf3nte-P.js.map
2534
+ //# sourceMappingURL=start-playground-C__Gy1yk.js.map