@mseep/claudian 2.0.25

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 (687) hide show
  1. package/.env.local.example +2 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/claude-code-review.yml +57 -0
  4. package/.github/workflows/claude.yml +50 -0
  5. package/.github/workflows/duplicate-issues.yml +22 -0
  6. package/.github/workflows/release.yml +73 -0
  7. package/.github/workflows/stale.yml +21 -0
  8. package/.node-version +1 -0
  9. package/AGENTS.md +3 -0
  10. package/CLAUDE.md +80 -0
  11. package/LICENSE +21 -0
  12. package/README.md +190 -0
  13. package/assets/Preview.png +0 -0
  14. package/assets/sponsors/MOMA.png +0 -0
  15. package/bun.lock +1618 -0
  16. package/esbuild.config.mjs +195 -0
  17. package/eslint.config.mjs +143 -0
  18. package/jest.config.js +41 -0
  19. package/main.js +104915 -0
  20. package/manifest.json +10 -0
  21. package/package.json +65 -0
  22. package/scripts/build-css.mjs +119 -0
  23. package/scripts/build.mjs +19 -0
  24. package/scripts/postinstall.mjs +25 -0
  25. package/scripts/rendererSafeUnref.js +205 -0
  26. package/scripts/run-jest.js +19 -0
  27. package/scripts/sync-version.js +16 -0
  28. package/src/app/settings/ClaudianSettingsStorage.ts +435 -0
  29. package/src/app/settings/defaultSettings.ts +54 -0
  30. package/src/app/storage/SharedStorageService.ts +106 -0
  31. package/src/core/CLAUDE.md +84 -0
  32. package/src/core/auxiliary/AuxQueryRunner.ts +11 -0
  33. package/src/core/auxiliary/QueryBackedInlineEditService.ts +73 -0
  34. package/src/core/auxiliary/QueryBackedInstructionRefineService.ts +78 -0
  35. package/src/core/auxiliary/QueryBackedTitleGenerationService.ts +90 -0
  36. package/src/core/bootstrap/SessionStorage.ts +179 -0
  37. package/src/core/bootstrap/StoragePaths.ts +7 -0
  38. package/src/core/bootstrap/storage.ts +20 -0
  39. package/src/core/commands/builtInCommands.ts +141 -0
  40. package/src/core/mcp/McpConfigParser.ts +102 -0
  41. package/src/core/mcp/McpServerManager.ts +119 -0
  42. package/src/core/mcp/McpTester.ts +310 -0
  43. package/src/core/prompt/inlineEdit.ts +252 -0
  44. package/src/core/prompt/instructionRefine.ts +72 -0
  45. package/src/core/prompt/mainAgent.ts +213 -0
  46. package/src/core/prompt/titleGeneration.ts +44 -0
  47. package/src/core/providers/ProviderRegistry.ts +253 -0
  48. package/src/core/providers/ProviderSettingsCoordinator.ts +434 -0
  49. package/src/core/providers/ProviderWorkspaceRegistry.ts +119 -0
  50. package/src/core/providers/commands/ProviderCommandCatalog.ts +21 -0
  51. package/src/core/providers/commands/ProviderCommandEntry.ts +33 -0
  52. package/src/core/providers/commands/hiddenCommands.ts +74 -0
  53. package/src/core/providers/modelRouting.ts +17 -0
  54. package/src/core/providers/modelSelection.ts +72 -0
  55. package/src/core/providers/providerConfig.ts +34 -0
  56. package/src/core/providers/providerEnvironment.ts +364 -0
  57. package/src/core/providers/types.ts +544 -0
  58. package/src/core/runtime/ChatRuntime.ts +66 -0
  59. package/src/core/runtime/QueuedTurn.ts +97 -0
  60. package/src/core/runtime/types.ts +118 -0
  61. package/src/core/security/ApprovalManager.ts +142 -0
  62. package/src/core/storage/HomeFileAdapter.ts +75 -0
  63. package/src/core/storage/VaultFileAdapter.ts +132 -0
  64. package/src/core/tools/todo.ts +65 -0
  65. package/src/core/tools/toolIcons.ts +80 -0
  66. package/src/core/tools/toolInput.ts +119 -0
  67. package/src/core/tools/toolNames.ts +149 -0
  68. package/src/core/tools/toolResultContent.ts +26 -0
  69. package/src/core/types/agent.ts +28 -0
  70. package/src/core/types/chat.ts +177 -0
  71. package/src/core/types/diff.ts +31 -0
  72. package/src/core/types/index.ts +78 -0
  73. package/src/core/types/mcp.ts +97 -0
  74. package/src/core/types/plugins.ts +9 -0
  75. package/src/core/types/provider.ts +1 -0
  76. package/src/core/types/settings.ts +152 -0
  77. package/src/core/types/tools.ts +80 -0
  78. package/src/features/chat/CLAUDE.md +136 -0
  79. package/src/features/chat/ClaudianView.ts +762 -0
  80. package/src/features/chat/constants.ts +114 -0
  81. package/src/features/chat/controllers/BrowserSelectionController.ts +295 -0
  82. package/src/features/chat/controllers/CanvasSelectionController.ts +142 -0
  83. package/src/features/chat/controllers/ConversationController.ts +1103 -0
  84. package/src/features/chat/controllers/InputController.ts +1707 -0
  85. package/src/features/chat/controllers/NavigationController.ts +209 -0
  86. package/src/features/chat/controllers/SelectionController.ts +430 -0
  87. package/src/features/chat/controllers/StreamController.ts +1560 -0
  88. package/src/features/chat/controllers/contextRowVisibility.ts +18 -0
  89. package/src/features/chat/rendering/DiffRenderer.ts +134 -0
  90. package/src/features/chat/rendering/InlineAskUserQuestion.ts +702 -0
  91. package/src/features/chat/rendering/InlineExitPlanMode.ts +263 -0
  92. package/src/features/chat/rendering/InlinePlanApproval.ts +183 -0
  93. package/src/features/chat/rendering/MessageRenderer.ts +907 -0
  94. package/src/features/chat/rendering/SubagentRenderer.ts +678 -0
  95. package/src/features/chat/rendering/ThinkingBlockRenderer.ts +126 -0
  96. package/src/features/chat/rendering/TodoListRenderer.ts +5 -0
  97. package/src/features/chat/rendering/ToolCallRenderer.ts +1161 -0
  98. package/src/features/chat/rendering/WriteEditRenderer.ts +232 -0
  99. package/src/features/chat/rendering/collapsible.ts +101 -0
  100. package/src/features/chat/rendering/subagentLifecycleResolution.ts +25 -0
  101. package/src/features/chat/rendering/todoUtils.ts +29 -0
  102. package/src/features/chat/rewind.ts +31 -0
  103. package/src/features/chat/services/BangBashService.ts +56 -0
  104. package/src/features/chat/services/SubagentManager.ts +1107 -0
  105. package/src/features/chat/state/ChatState.ts +436 -0
  106. package/src/features/chat/state/types.ts +138 -0
  107. package/src/features/chat/tabs/Tab.ts +1886 -0
  108. package/src/features/chat/tabs/TabBar.ts +179 -0
  109. package/src/features/chat/tabs/TabManager.ts +1021 -0
  110. package/src/features/chat/tabs/providerResolution.ts +34 -0
  111. package/src/features/chat/tabs/types.ts +287 -0
  112. package/src/features/chat/ui/BangBashModeManager.ts +121 -0
  113. package/src/features/chat/ui/FileContext.ts +385 -0
  114. package/src/features/chat/ui/ImageContext.ts +366 -0
  115. package/src/features/chat/ui/InputToolbar.ts +1244 -0
  116. package/src/features/chat/ui/InstructionModeManager.ts +158 -0
  117. package/src/features/chat/ui/NavigationSidebar.ts +126 -0
  118. package/src/features/chat/ui/StatusPanel.ts +589 -0
  119. package/src/features/chat/ui/file-context/state/FileContextState.ts +83 -0
  120. package/src/features/chat/ui/file-context/view/FileChipsView.ts +70 -0
  121. package/src/features/chat/ui/textareaResize.ts +47 -0
  122. package/src/features/chat/utils/usageInfo.ts +26 -0
  123. package/src/features/inline-edit/ui/InlineEditModal.ts +895 -0
  124. package/src/features/inline-edit/ui/inlineEditMarkdownPreview.ts +55 -0
  125. package/src/features/settings/ClaudianSettings.ts +672 -0
  126. package/src/features/settings/keyboardNavigation.ts +60 -0
  127. package/src/features/settings/ui/EnvSnippetManager.ts +430 -0
  128. package/src/features/settings/ui/EnvironmentSettingsSection.ts +85 -0
  129. package/src/features/settings/ui/McpServerModal.ts +335 -0
  130. package/src/features/settings/ui/McpSettingsManager.ts +400 -0
  131. package/src/features/settings/ui/McpTestModal.ts +346 -0
  132. package/src/i18n/constants.ts +58 -0
  133. package/src/i18n/i18n.ts +140 -0
  134. package/src/i18n/locales/de.json +322 -0
  135. package/src/i18n/locales/en.json +322 -0
  136. package/src/i18n/locales/es.json +322 -0
  137. package/src/i18n/locales/fr.json +322 -0
  138. package/src/i18n/locales/ja.json +322 -0
  139. package/src/i18n/locales/ko.json +322 -0
  140. package/src/i18n/locales/pt.json +322 -0
  141. package/src/i18n/locales/ru.json +322 -0
  142. package/src/i18n/locales/zh-CN.json +322 -0
  143. package/src/i18n/locales/zh-TW.json +322 -0
  144. package/src/i18n/types.ts +248 -0
  145. package/src/main.ts +772 -0
  146. package/src/providers/acp/AcpClientConnection.ts +361 -0
  147. package/src/providers/acp/AcpJsonRpcTransport.ts +427 -0
  148. package/src/providers/acp/AcpSessionConfig.ts +139 -0
  149. package/src/providers/acp/AcpSessionUpdateNormalizer.ts +371 -0
  150. package/src/providers/acp/AcpSubprocess.ts +155 -0
  151. package/src/providers/acp/AcpToolStreamAdapter.ts +132 -0
  152. package/src/providers/acp/buildAcpUsageInfo.ts +41 -0
  153. package/src/providers/acp/index.ts +9 -0
  154. package/src/providers/acp/methodNames.ts +50 -0
  155. package/src/providers/acp/types.ts +566 -0
  156. package/src/providers/claude/CLAUDE.md +78 -0
  157. package/src/providers/claude/agents/AgentManager.ts +225 -0
  158. package/src/providers/claude/agents/AgentStorage.ts +101 -0
  159. package/src/providers/claude/app/ClaudeWorkspaceServices.ts +90 -0
  160. package/src/providers/claude/auxiliary/ClaudeInlineEditService.ts +122 -0
  161. package/src/providers/claude/auxiliary/ClaudeInstructionRefineService.ts +90 -0
  162. package/src/providers/claude/auxiliary/ClaudeTitleGenerationService.ts +129 -0
  163. package/src/providers/claude/auxiliary/extractAssistantText.ts +28 -0
  164. package/src/providers/claude/capabilities.ts +17 -0
  165. package/src/providers/claude/cli/findClaudeCLIPath.ts +261 -0
  166. package/src/providers/claude/commands/ClaudeCommandCatalog.ts +149 -0
  167. package/src/providers/claude/commands/probeRuntimeCommands.ts +86 -0
  168. package/src/providers/claude/env/ClaudeSettingsReconciler.ts +90 -0
  169. package/src/providers/claude/env/claudeModelEnv.ts +86 -0
  170. package/src/providers/claude/history/ClaudeConversationHistoryService.ts +446 -0
  171. package/src/providers/claude/history/ClaudeHistoryStore.ts +170 -0
  172. package/src/providers/claude/history/sdkAsyncSubagent.ts +92 -0
  173. package/src/providers/claude/history/sdkBranchFilter.ts +271 -0
  174. package/src/providers/claude/history/sdkHistoryTypes.ts +61 -0
  175. package/src/providers/claude/history/sdkMessageParsing.ts +413 -0
  176. package/src/providers/claude/history/sdkSessionPaths.ts +98 -0
  177. package/src/providers/claude/history/sdkSubagentSidecar.ts +261 -0
  178. package/src/providers/claude/hooks/SubagentHooks.ts +31 -0
  179. package/src/providers/claude/modelLabels.ts +77 -0
  180. package/src/providers/claude/modelOptions.ts +113 -0
  181. package/src/providers/claude/modelSelection.ts +17 -0
  182. package/src/providers/claude/plugins/PluginManager.ts +194 -0
  183. package/src/providers/claude/prompt/ClaudeTurnEncoder.ts +46 -0
  184. package/src/providers/claude/registration.ts +39 -0
  185. package/src/providers/claude/runtime/ClaudeApprovalHandler.ts +153 -0
  186. package/src/providers/claude/runtime/ClaudeChatRuntime.ts +1812 -0
  187. package/src/providers/claude/runtime/ClaudeCliResolver.ts +94 -0
  188. package/src/providers/claude/runtime/ClaudeDynamicUpdates.ts +164 -0
  189. package/src/providers/claude/runtime/ClaudeMessageChannel.ts +209 -0
  190. package/src/providers/claude/runtime/ClaudeQueryOptionsBuilder.ts +315 -0
  191. package/src/providers/claude/runtime/ClaudeRewindService.ts +220 -0
  192. package/src/providers/claude/runtime/ClaudeSessionManager.ts +92 -0
  193. package/src/providers/claude/runtime/ClaudeTaskResultInterpreter.ts +172 -0
  194. package/src/providers/claude/runtime/ClaudeUserMessageFactory.ts +83 -0
  195. package/src/providers/claude/runtime/claudeColdStartQuery.ts +152 -0
  196. package/src/providers/claude/runtime/customSpawn.ts +87 -0
  197. package/src/providers/claude/runtime/types.ts +134 -0
  198. package/src/providers/claude/sdk/messages.ts +17 -0
  199. package/src/providers/claude/sdk/toolResultContent.ts +4 -0
  200. package/src/providers/claude/sdk/typeGuards.ts +14 -0
  201. package/src/providers/claude/sdk/types.ts +15 -0
  202. package/src/providers/claude/security/ClaudePermissionUpdates.ts +44 -0
  203. package/src/providers/claude/settings.ts +138 -0
  204. package/src/providers/claude/storage/AgentVaultStorage.ts +101 -0
  205. package/src/providers/claude/storage/CCSettingsStorage.ts +153 -0
  206. package/src/providers/claude/storage/ClaudianSettingsStorage.ts +6 -0
  207. package/src/providers/claude/storage/McpStorage.ts +139 -0
  208. package/src/providers/claude/storage/SessionStorage.ts +5 -0
  209. package/src/providers/claude/storage/SkillStorage.ts +61 -0
  210. package/src/providers/claude/storage/SlashCommandStorage.ts +96 -0
  211. package/src/providers/claude/storage/StorageService.ts +185 -0
  212. package/src/providers/claude/stream/toolInputStreamState.ts +318 -0
  213. package/src/providers/claude/stream/transformClaudeMessage.ts +586 -0
  214. package/src/providers/claude/types/agent.ts +2 -0
  215. package/src/providers/claude/types/models.ts +168 -0
  216. package/src/providers/claude/types/plugins.ts +14 -0
  217. package/src/providers/claude/types/providerState.ts +16 -0
  218. package/src/providers/claude/types/settings.ts +98 -0
  219. package/src/providers/claude/ui/AgentSettings.ts +389 -0
  220. package/src/providers/claude/ui/ClaudeChatUIConfig.ts +113 -0
  221. package/src/providers/claude/ui/ClaudeSettingsTab.ts +407 -0
  222. package/src/providers/claude/ui/PluginSettingsManager.ts +149 -0
  223. package/src/providers/claude/ui/SlashCommandSettings.ts +527 -0
  224. package/src/providers/codex/CLAUDE.md +64 -0
  225. package/src/providers/codex/agents/CodexAgentMentionProvider.ts +33 -0
  226. package/src/providers/codex/app/CodexWorkspaceServices.ts +76 -0
  227. package/src/providers/codex/auxiliary/CodexInlineEditService.ts +9 -0
  228. package/src/providers/codex/auxiliary/CodexInstructionRefineService.ts +9 -0
  229. package/src/providers/codex/auxiliary/CodexTaskResultInterpreter.ts +29 -0
  230. package/src/providers/codex/auxiliary/CodexTitleGenerationService.ts +22 -0
  231. package/src/providers/codex/capabilities.ts +16 -0
  232. package/src/providers/codex/commands/CodexSkillCatalog.ts +180 -0
  233. package/src/providers/codex/env/CodexSettingsReconciler.ts +68 -0
  234. package/src/providers/codex/history/CodexConversationHistoryService.ts +212 -0
  235. package/src/providers/codex/history/CodexHistoryStore.ts +1672 -0
  236. package/src/providers/codex/modelOptions.ts +99 -0
  237. package/src/providers/codex/modelSelection.ts +17 -0
  238. package/src/providers/codex/normalization/codexSubagentNormalization.ts +227 -0
  239. package/src/providers/codex/normalization/codexToolNormalization.ts +390 -0
  240. package/src/providers/codex/prompt/encodeCodexTurn.ts +57 -0
  241. package/src/providers/codex/registration.ts +29 -0
  242. package/src/providers/codex/runtime/CodexAppServerProcess.ts +105 -0
  243. package/src/providers/codex/runtime/CodexAuxQueryRunner.ts +180 -0
  244. package/src/providers/codex/runtime/CodexBinaryLocator.ts +49 -0
  245. package/src/providers/codex/runtime/CodexChatRuntime.ts +1296 -0
  246. package/src/providers/codex/runtime/CodexCliResolver.ts +65 -0
  247. package/src/providers/codex/runtime/CodexExecutionTargetResolver.ts +104 -0
  248. package/src/providers/codex/runtime/CodexLaunchSpecBuilder.ts +85 -0
  249. package/src/providers/codex/runtime/CodexNotificationRouter.ts +1033 -0
  250. package/src/providers/codex/runtime/CodexPathMapper.ts +155 -0
  251. package/src/providers/codex/runtime/CodexRpcTransport.ts +171 -0
  252. package/src/providers/codex/runtime/CodexRuntimeContext.ts +109 -0
  253. package/src/providers/codex/runtime/CodexServerRequestRouter.ts +331 -0
  254. package/src/providers/codex/runtime/CodexSessionFileTail.ts +792 -0
  255. package/src/providers/codex/runtime/CodexSessionManager.ts +39 -0
  256. package/src/providers/codex/runtime/codexAppServerSupport.ts +58 -0
  257. package/src/providers/codex/runtime/codexAppServerTypes.ts +705 -0
  258. package/src/providers/codex/runtime/codexLaunchTypes.ts +30 -0
  259. package/src/providers/codex/settings.ts +236 -0
  260. package/src/providers/codex/skills/CodexSkillListingService.ts +173 -0
  261. package/src/providers/codex/storage/CodexSkillStorage.ts +250 -0
  262. package/src/providers/codex/storage/CodexSubagentStorage.ts +212 -0
  263. package/src/providers/codex/types/index.ts +16 -0
  264. package/src/providers/codex/types/models.ts +46 -0
  265. package/src/providers/codex/types/subagent.ts +23 -0
  266. package/src/providers/codex/ui/CodexChatUIConfig.ts +128 -0
  267. package/src/providers/codex/ui/CodexSettingsTab.ts +432 -0
  268. package/src/providers/codex/ui/CodexSkillSettings.ts +275 -0
  269. package/src/providers/codex/ui/CodexSubagentSettings.ts +400 -0
  270. package/src/providers/defaultProviderConfigs.ts +14 -0
  271. package/src/providers/index.ts +30 -0
  272. package/src/providers/opencode/agents/OpencodeAgentMentionProvider.ts +42 -0
  273. package/src/providers/opencode/app/OpencodeRuntimeCommandLoader.ts +67 -0
  274. package/src/providers/opencode/app/OpencodeWorkspaceServices.ts +55 -0
  275. package/src/providers/opencode/auxiliary/OpencodeInlineEditService.ts +13 -0
  276. package/src/providers/opencode/auxiliary/OpencodeInstructionRefineService.ts +12 -0
  277. package/src/providers/opencode/auxiliary/OpencodeTaskResultInterpreter.ts +29 -0
  278. package/src/providers/opencode/auxiliary/OpencodeTitleGenerationService.ts +27 -0
  279. package/src/providers/opencode/capabilities.ts +16 -0
  280. package/src/providers/opencode/commands/OpencodeCommandCatalog.ts +92 -0
  281. package/src/providers/opencode/discoveryState.ts +135 -0
  282. package/src/providers/opencode/env/OpencodeSettingsReconciler.ts +178 -0
  283. package/src/providers/opencode/history/OpencodeConversationHistoryService.ts +84 -0
  284. package/src/providers/opencode/history/OpencodeHistoryStore.ts +472 -0
  285. package/src/providers/opencode/history/OpencodeSqliteReader.ts +284 -0
  286. package/src/providers/opencode/internal/compareCollections.ts +72 -0
  287. package/src/providers/opencode/internal/providerProjection.ts +15 -0
  288. package/src/providers/opencode/models.ts +378 -0
  289. package/src/providers/opencode/modes.ts +150 -0
  290. package/src/providers/opencode/normalization/opencodeToolNormalization.ts +406 -0
  291. package/src/providers/opencode/registration.ts +27 -0
  292. package/src/providers/opencode/runtime/OpencodeAuxQueryRunner.ts +436 -0
  293. package/src/providers/opencode/runtime/OpencodeChatRuntime.ts +1603 -0
  294. package/src/providers/opencode/runtime/OpencodeCliResolver.ts +57 -0
  295. package/src/providers/opencode/runtime/OpencodeLaunchArtifacts.ts +231 -0
  296. package/src/providers/opencode/runtime/OpencodePaths.ts +113 -0
  297. package/src/providers/opencode/runtime/OpencodeRuntimeEnvironment.ts +18 -0
  298. package/src/providers/opencode/runtime/buildOpencodePrompt.ts +66 -0
  299. package/src/providers/opencode/settings.ts +427 -0
  300. package/src/providers/opencode/storage/OpencodeAgentStorage.ts +346 -0
  301. package/src/providers/opencode/types/agent.ts +37 -0
  302. package/src/providers/opencode/types/index.ts +9 -0
  303. package/src/providers/opencode/ui/OpencodeAgentSettings.ts +579 -0
  304. package/src/providers/opencode/ui/OpencodeChatUIConfig.ts +316 -0
  305. package/src/providers/opencode/ui/OpencodeSettingsTab.ts +674 -0
  306. package/src/providers/pi/app/PiRuntimeCommandLoader.ts +57 -0
  307. package/src/providers/pi/app/PiWorkspaceServices.ts +39 -0
  308. package/src/providers/pi/auxiliary/PiInlineEditService.ts +9 -0
  309. package/src/providers/pi/auxiliary/PiInstructionRefineService.ts +9 -0
  310. package/src/providers/pi/auxiliary/PiTaskResultInterpreter.ts +29 -0
  311. package/src/providers/pi/auxiliary/PiTitleGenerationService.ts +19 -0
  312. package/src/providers/pi/capabilities.ts +16 -0
  313. package/src/providers/pi/commands/PiCommandCatalog.ts +92 -0
  314. package/src/providers/pi/env/PiSettingsReconciler.ts +180 -0
  315. package/src/providers/pi/history/PiConversationHistoryService.ts +123 -0
  316. package/src/providers/pi/history/PiHistoryStore.ts +664 -0
  317. package/src/providers/pi/internal/compareCollections.ts +4 -0
  318. package/src/providers/pi/internal/providerProjection.ts +18 -0
  319. package/src/providers/pi/models.ts +302 -0
  320. package/src/providers/pi/normalizations/piEventNormalization.ts +211 -0
  321. package/src/providers/pi/normalizations/piToolNormalization.ts +97 -0
  322. package/src/providers/pi/registration.ts +30 -0
  323. package/src/providers/pi/runtime/PiAuxQueryRunner.ts +216 -0
  324. package/src/providers/pi/runtime/PiChatRuntime.ts +1064 -0
  325. package/src/providers/pi/runtime/PiCliResolver.ts +53 -0
  326. package/src/providers/pi/runtime/PiExtensionUiBridge.ts +161 -0
  327. package/src/providers/pi/runtime/PiJsonl.ts +71 -0
  328. package/src/providers/pi/runtime/PiLaunchSpec.ts +70 -0
  329. package/src/providers/pi/runtime/PiModelDiscoveryService.ts +92 -0
  330. package/src/providers/pi/runtime/PiRpcPayloads.ts +18 -0
  331. package/src/providers/pi/runtime/PiRpcTransport.ts +243 -0
  332. package/src/providers/pi/runtime/PiSubprocess.ts +159 -0
  333. package/src/providers/pi/runtime/buildPiPrompt.ts +62 -0
  334. package/src/providers/pi/runtime/buildPiUsageInfo.ts +69 -0
  335. package/src/providers/pi/settings.ts +468 -0
  336. package/src/providers/pi/types.ts +64 -0
  337. package/src/providers/pi/ui/ObsidianPiExtensionUiRenderer.ts +251 -0
  338. package/src/providers/pi/ui/PiChatUIConfig.ts +265 -0
  339. package/src/providers/pi/ui/PiExtensionUiRenderer.ts +12 -0
  340. package/src/providers/pi/ui/PiSettingsTab.ts +642 -0
  341. package/src/shared/components/ResumeSessionDropdown.ts +185 -0
  342. package/src/shared/components/SelectableDropdown.ts +140 -0
  343. package/src/shared/components/SelectionHighlight.ts +77 -0
  344. package/src/shared/components/SlashCommandDropdown.ts +421 -0
  345. package/src/shared/icons.ts +180 -0
  346. package/src/shared/mention/MentionDropdownController.ts +627 -0
  347. package/src/shared/mention/VaultMentionCache.ts +106 -0
  348. package/src/shared/mention/VaultMentionDataProvider.ts +51 -0
  349. package/src/shared/mention/types.ts +67 -0
  350. package/src/shared/modals/ConfirmModal.ts +60 -0
  351. package/src/shared/modals/ForkTargetModal.ts +47 -0
  352. package/src/shared/modals/InstructionConfirmModal.ts +281 -0
  353. package/src/style/CLAUDE.md +49 -0
  354. package/src/style/accessibility.css +40 -0
  355. package/src/style/base/animations.css +44 -0
  356. package/src/style/base/container.css +20 -0
  357. package/src/style/base/variables.css +46 -0
  358. package/src/style/base/visibility.css +15 -0
  359. package/src/style/components/code.css +97 -0
  360. package/src/style/components/context-footer.css +76 -0
  361. package/src/style/components/header.css +27 -0
  362. package/src/style/components/history.css +221 -0
  363. package/src/style/components/input.css +312 -0
  364. package/src/style/components/messages.css +262 -0
  365. package/src/style/components/nav-sidebar.css +58 -0
  366. package/src/style/components/status-panel.css +202 -0
  367. package/src/style/components/subagent.css +248 -0
  368. package/src/style/components/tabs.css +112 -0
  369. package/src/style/components/thinking.css +88 -0
  370. package/src/style/components/toolcalls.css +278 -0
  371. package/src/style/features/ask-user-question.css +315 -0
  372. package/src/style/features/diff.css +197 -0
  373. package/src/style/features/file-context.css +188 -0
  374. package/src/style/features/file-link.css +22 -0
  375. package/src/style/features/image-context.css +179 -0
  376. package/src/style/features/image-embed.css +40 -0
  377. package/src/style/features/image-modal.css +52 -0
  378. package/src/style/features/inline-edit.css +278 -0
  379. package/src/style/features/plan-mode.css +103 -0
  380. package/src/style/features/resume-session.css +119 -0
  381. package/src/style/features/slash-commands.css +91 -0
  382. package/src/style/index.css +63 -0
  383. package/src/style/modals/fork-target.css +21 -0
  384. package/src/style/modals/instruction.css +161 -0
  385. package/src/style/modals/mcp-modal.css +241 -0
  386. package/src/style/settings/agent-settings.css +2 -0
  387. package/src/style/settings/base.css +300 -0
  388. package/src/style/settings/env-snippets.css +366 -0
  389. package/src/style/settings/mcp-settings.css +211 -0
  390. package/src/style/settings/plugin-settings.css +164 -0
  391. package/src/style/settings/provider-model-picker.css +367 -0
  392. package/src/style/settings/slash-settings.css +16 -0
  393. package/src/style/toolbar/external-context.css +177 -0
  394. package/src/style/toolbar/mcp-selector.css +176 -0
  395. package/src/style/toolbar/mode-selector.css +19 -0
  396. package/src/style/toolbar/model-selector.css +99 -0
  397. package/src/style/toolbar/permission-toggle.css +56 -0
  398. package/src/style/toolbar/service-tier-toggle.css +39 -0
  399. package/src/style/toolbar/thinking-selector.css +83 -0
  400. package/src/types/smol-toml.d.ts +4 -0
  401. package/src/utils/agent.ts +50 -0
  402. package/src/utils/animationFrame.ts +46 -0
  403. package/src/utils/browser.ts +46 -0
  404. package/src/utils/canvas.ts +14 -0
  405. package/src/utils/cliBinaryLocator.ts +97 -0
  406. package/src/utils/context.ts +117 -0
  407. package/src/utils/contextMentionResolver.ts +154 -0
  408. package/src/utils/date.ts +31 -0
  409. package/src/utils/diff.ts +384 -0
  410. package/src/utils/editor.ts +104 -0
  411. package/src/utils/electronCompat.ts +53 -0
  412. package/src/utils/env.ts +465 -0
  413. package/src/utils/externalContext.ts +143 -0
  414. package/src/utils/externalContextScanner.ts +135 -0
  415. package/src/utils/fileLink.ts +263 -0
  416. package/src/utils/frontmatter.ts +194 -0
  417. package/src/utils/imageEmbed.ts +139 -0
  418. package/src/utils/inlineEdit.ts +22 -0
  419. package/src/utils/interrupt.ts +23 -0
  420. package/src/utils/markdown.ts +25 -0
  421. package/src/utils/markdownMath.ts +130 -0
  422. package/src/utils/mcp.ts +96 -0
  423. package/src/utils/obsidianCompat.ts +23 -0
  424. package/src/utils/path.ts +342 -0
  425. package/src/utils/session.ts +240 -0
  426. package/src/utils/slashCommand.ts +152 -0
  427. package/src/utils/subagentJsonl.ts +52 -0
  428. package/src/utils/windowsCmdShim.ts +98 -0
  429. package/tests/__mocks__/claude-agent-sdk.ts +317 -0
  430. package/tests/__mocks__/codex-sdk.ts +88 -0
  431. package/tests/__mocks__/obsidian.ts +434 -0
  432. package/tests/helpers/mockElement.ts +403 -0
  433. package/tests/helpers/sdkMessages.ts +291 -0
  434. package/tests/integration/core/agent/ClaudianService.test.ts +1845 -0
  435. package/tests/integration/core/mcp/mcp.test.ts +905 -0
  436. package/tests/integration/features/chat/imagePersistence.test.ts +38 -0
  437. package/tests/integration/main.test.ts +1701 -0
  438. package/tests/setupWindow.ts +26 -0
  439. package/tests/tsconfig.json +7 -0
  440. package/tests/unit/core/commands/builtInCommands.test.ts +239 -0
  441. package/tests/unit/core/mcp/McpServerManager.test.ts +405 -0
  442. package/tests/unit/core/mcp/McpTester.test.ts +282 -0
  443. package/tests/unit/core/mcp/createNodeFetch.test.ts +188 -0
  444. package/tests/unit/core/providers/ProviderRegistry.test.ts +275 -0
  445. package/tests/unit/core/providers/ProviderSettingsCoordinator.test.ts +490 -0
  446. package/tests/unit/core/providers/ProviderWorkspaceRegistry.test.ts +84 -0
  447. package/tests/unit/core/providers/modelRouting.test.ts +91 -0
  448. package/tests/unit/core/providers/modelSelection.test.ts +155 -0
  449. package/tests/unit/core/providers/providerEnvironment.test.ts +162 -0
  450. package/tests/unit/core/providers/tabLifecycle.test.ts +217 -0
  451. package/tests/unit/core/security/ApprovalManager.test.ts +152 -0
  452. package/tests/unit/core/storage/VaultFileAdapter.test.ts +535 -0
  453. package/tests/unit/core/tools/todo.test.ts +227 -0
  454. package/tests/unit/core/tools/toolIcons.test.ts +75 -0
  455. package/tests/unit/core/tools/toolInput.test.ts +350 -0
  456. package/tests/unit/core/tools/toolNames.test.ts +464 -0
  457. package/tests/unit/core/types/mcp.test.ts +115 -0
  458. package/tests/unit/features/chat/ClaudianView.test.ts +404 -0
  459. package/tests/unit/features/chat/controllers/BrowserSelectionController.test.ts +179 -0
  460. package/tests/unit/features/chat/controllers/CanvasSelectionController.test.ts +216 -0
  461. package/tests/unit/features/chat/controllers/ConversationController.test.ts +2764 -0
  462. package/tests/unit/features/chat/controllers/InputController.test.ts +3188 -0
  463. package/tests/unit/features/chat/controllers/NavigationController.test.ts +640 -0
  464. package/tests/unit/features/chat/controllers/SelectionController.test.ts +695 -0
  465. package/tests/unit/features/chat/controllers/StreamController.test.ts +2534 -0
  466. package/tests/unit/features/chat/controllers/contextRowVisibility.test.ts +46 -0
  467. package/tests/unit/features/chat/controllers/index.test.ts +16 -0
  468. package/tests/unit/features/chat/rendering/DiffRenderer.test.ts +355 -0
  469. package/tests/unit/features/chat/rendering/InlineAskUserQuestion.test.ts +1035 -0
  470. package/tests/unit/features/chat/rendering/InlineExitPlanMode.test.ts +191 -0
  471. package/tests/unit/features/chat/rendering/InlinePlanApproval.test.ts +126 -0
  472. package/tests/unit/features/chat/rendering/MessageRenderer.test.ts +2004 -0
  473. package/tests/unit/features/chat/rendering/SubagentRenderer.test.ts +917 -0
  474. package/tests/unit/features/chat/rendering/ThinkingBlockRenderer.test.ts +124 -0
  475. package/tests/unit/features/chat/rendering/TodoListRenderer.test.ts +173 -0
  476. package/tests/unit/features/chat/rendering/ToolCallRenderer.test.ts +909 -0
  477. package/tests/unit/features/chat/rendering/WriteEditRenderer.test.ts +474 -0
  478. package/tests/unit/features/chat/rendering/collapsible.test.ts +158 -0
  479. package/tests/unit/features/chat/rendering/todoUtils.test.ts +105 -0
  480. package/tests/unit/features/chat/rewind.test.ts +56 -0
  481. package/tests/unit/features/chat/services/BangBashService.test.ts +142 -0
  482. package/tests/unit/features/chat/services/InstructionRefineService.test.ts +371 -0
  483. package/tests/unit/features/chat/services/SubagentManager.test.ts +1759 -0
  484. package/tests/unit/features/chat/services/TitleGenerationService.test.ts +480 -0
  485. package/tests/unit/features/chat/state/ChatState.test.ts +581 -0
  486. package/tests/unit/features/chat/tabs/Tab.test.ts +4287 -0
  487. package/tests/unit/features/chat/tabs/TabBar.test.ts +357 -0
  488. package/tests/unit/features/chat/tabs/TabManager.test.ts +2962 -0
  489. package/tests/unit/features/chat/tabs/index.test.ts +11 -0
  490. package/tests/unit/features/chat/ui/BangBashModeManager.test.ts +321 -0
  491. package/tests/unit/features/chat/ui/ExternalContextSelector.test.ts +555 -0
  492. package/tests/unit/features/chat/ui/FileContextManager.test.ts +876 -0
  493. package/tests/unit/features/chat/ui/ImageContext.test.ts +777 -0
  494. package/tests/unit/features/chat/ui/InputToolbar.test.ts +1139 -0
  495. package/tests/unit/features/chat/ui/InstructionModeManager.test.ts +243 -0
  496. package/tests/unit/features/chat/ui/NavigationSidebar.test.ts +570 -0
  497. package/tests/unit/features/chat/ui/StatusPanel.test.ts +953 -0
  498. package/tests/unit/features/chat/ui/file-context/state/FileContextState.test.ts +155 -0
  499. package/tests/unit/features/chat/ui/textareaResize.test.ts +102 -0
  500. package/tests/unit/features/chat/utils/usageInfo.test.ts +56 -0
  501. package/tests/unit/features/inline-edit/InlineEditService.test.ts +1199 -0
  502. package/tests/unit/features/inline-edit/ui/InlineEditModal.openAndWait.test.ts +1482 -0
  503. package/tests/unit/features/inline-edit/ui/InlineEditModal.test.ts +495 -0
  504. package/tests/unit/features/inline-edit/ui/inlineEditMarkdownPreview.test.ts +92 -0
  505. package/tests/unit/features/settings/AgentSettings.test.ts +82 -0
  506. package/tests/unit/features/settings/keyboardNavigation.test.ts +73 -0
  507. package/tests/unit/features/settings/ui/CodexSkillSettings.test.ts +294 -0
  508. package/tests/unit/features/settings/ui/CodexSubagentSettings.test.ts +207 -0
  509. package/tests/unit/i18n/constants.test.ts +43 -0
  510. package/tests/unit/i18n/i18n.test.ts +244 -0
  511. package/tests/unit/i18n/locales.test.ts +134 -0
  512. package/tests/unit/providers/acp/AcpClientConnection.test.ts +248 -0
  513. package/tests/unit/providers/acp/AcpJsonRpcTransport.test.ts +186 -0
  514. package/tests/unit/providers/acp/AcpSessionConfig.test.ts +247 -0
  515. package/tests/unit/providers/acp/AcpSessionUpdateNormalizer.test.ts +145 -0
  516. package/tests/unit/providers/acp/AcpSubprocess.test.ts +105 -0
  517. package/tests/unit/providers/acp/buildAcpUsageInfo.test.ts +51 -0
  518. package/tests/unit/providers/claude/agents/AgentManager.test.ts +590 -0
  519. package/tests/unit/providers/claude/agents/AgentStorage.test.ts +434 -0
  520. package/tests/unit/providers/claude/agents/index.test.ts +10 -0
  521. package/tests/unit/providers/claude/commands/ClaudeCommandCatalog.test.ts +396 -0
  522. package/tests/unit/providers/claude/commands/probeRuntimeCommands.test.ts +92 -0
  523. package/tests/unit/providers/claude/env/ClaudeSettingsReconciler.test.ts +57 -0
  524. package/tests/unit/providers/claude/env/claudeModelEnv.test.ts +228 -0
  525. package/tests/unit/providers/claude/hooks/SubagentHooks.test.ts +83 -0
  526. package/tests/unit/providers/claude/plugins/PluginManager.test.ts +832 -0
  527. package/tests/unit/providers/claude/plugins/index.test.ts +7 -0
  528. package/tests/unit/providers/claude/prompt/ClaudeTurnEncoder.test.ts +145 -0
  529. package/tests/unit/providers/claude/prompt/instructionRefine.test.ts +185 -0
  530. package/tests/unit/providers/claude/prompt/systemPrompt.test.ts +163 -0
  531. package/tests/unit/providers/claude/prompt/titleGeneration.test.ts +20 -0
  532. package/tests/unit/providers/claude/runtime/ClaudeTaskResultInterpreter.test.ts +28 -0
  533. package/tests/unit/providers/claude/runtime/ClaudianService.test.ts +3796 -0
  534. package/tests/unit/providers/claude/runtime/MessageChannel.test.ts +421 -0
  535. package/tests/unit/providers/claude/runtime/QueryOptionsBuilder.test.ts +775 -0
  536. package/tests/unit/providers/claude/runtime/SessionManager.test.ts +182 -0
  537. package/tests/unit/providers/claude/runtime/claudeColdStartQuery.test.ts +331 -0
  538. package/tests/unit/providers/claude/runtime/customSpawn.test.ts +374 -0
  539. package/tests/unit/providers/claude/runtime/index.test.ts +13 -0
  540. package/tests/unit/providers/claude/runtime/types.test.ts +190 -0
  541. package/tests/unit/providers/claude/sdk/typeGuards.test.ts +50 -0
  542. package/tests/unit/providers/claude/security/ClaudePermissionUpdates.test.ts +198 -0
  543. package/tests/unit/providers/claude/storage/AgentVaultStorage.test.ts +413 -0
  544. package/tests/unit/providers/claude/storage/CCSettingsStorage.test.ts +408 -0
  545. package/tests/unit/providers/claude/storage/ClaudianSettingsStorage.test.ts +653 -0
  546. package/tests/unit/providers/claude/storage/McpStorage.test.ts +619 -0
  547. package/tests/unit/providers/claude/storage/SessionStorage.test.ts +680 -0
  548. package/tests/unit/providers/claude/storage/SkillStorage.test.ts +275 -0
  549. package/tests/unit/providers/claude/storage/SlashCommandStorage.test.ts +612 -0
  550. package/tests/unit/providers/claude/storage/storage.test.ts +360 -0
  551. package/tests/unit/providers/claude/storage/storageService.convenience.test.ts +447 -0
  552. package/tests/unit/providers/claude/stream/transformSDKMessage.test.ts +1729 -0
  553. package/tests/unit/providers/claude/types/types.test.ts +726 -0
  554. package/tests/unit/providers/claude/ui/ClaudeChatUIConfig.test.ts +173 -0
  555. package/tests/unit/providers/claude/ui/ClaudeSettingsTab.test.ts +466 -0
  556. package/tests/unit/providers/codex/agents/CodexAgentMentionProvider.test.ts +89 -0
  557. package/tests/unit/providers/codex/auxiliary/CodexInstructionRefineService.test.ts +81 -0
  558. package/tests/unit/providers/codex/capabilities.test.ts +39 -0
  559. package/tests/unit/providers/codex/commands/CodexSkillCatalog.test.ts +413 -0
  560. package/tests/unit/providers/codex/env/CodexSettingsReconciler.test.ts +106 -0
  561. package/tests/unit/providers/codex/fixtures/codex-session-abort.jsonl +9 -0
  562. package/tests/unit/providers/codex/fixtures/codex-session-agent-lifecycle.jsonl +12 -0
  563. package/tests/unit/providers/codex/fixtures/codex-session-persisted-tools.jsonl +15 -0
  564. package/tests/unit/providers/codex/fixtures/codex-session-simple.jsonl +10 -0
  565. package/tests/unit/providers/codex/fixtures/codex-session-tools.jsonl +12 -0
  566. package/tests/unit/providers/codex/fixtures/codex-session-websearch-persisted.jsonl +3 -0
  567. package/tests/unit/providers/codex/fixtures/codex-session-websearch.jsonl +7 -0
  568. package/tests/unit/providers/codex/history/CodexConversationHistoryService.test.ts +690 -0
  569. package/tests/unit/providers/codex/history/CodexHistoryStore.test.ts +2202 -0
  570. package/tests/unit/providers/codex/normalization/codexSubagentNormalization.test.ts +81 -0
  571. package/tests/unit/providers/codex/normalization/codexToolNormalization.test.ts +322 -0
  572. package/tests/unit/providers/codex/prompt/encodeCodexTurn.test.ts +168 -0
  573. package/tests/unit/providers/codex/runtime/CodexAppServerProcess.test.ts +255 -0
  574. package/tests/unit/providers/codex/runtime/CodexAuxQueryRunner.test.ts +130 -0
  575. package/tests/unit/providers/codex/runtime/CodexBinaryLocator.test.ts +90 -0
  576. package/tests/unit/providers/codex/runtime/CodexChatRuntime.test.ts +2445 -0
  577. package/tests/unit/providers/codex/runtime/CodexCliResolver.test.ts +103 -0
  578. package/tests/unit/providers/codex/runtime/CodexExecutionTargetResolver.test.ts +105 -0
  579. package/tests/unit/providers/codex/runtime/CodexLaunchSpecBuilder.test.ts +150 -0
  580. package/tests/unit/providers/codex/runtime/CodexNotificationRouter.test.ts +1248 -0
  581. package/tests/unit/providers/codex/runtime/CodexPathMapper.test.ts +51 -0
  582. package/tests/unit/providers/codex/runtime/CodexRpcTransport.test.ts +220 -0
  583. package/tests/unit/providers/codex/runtime/CodexRuntimeContext.test.ts +107 -0
  584. package/tests/unit/providers/codex/runtime/CodexServerRequestRouter.test.ts +537 -0
  585. package/tests/unit/providers/codex/runtime/CodexSessionFileTail.test.ts +1305 -0
  586. package/tests/unit/providers/codex/runtime/CodexSessionManager.test.ts +82 -0
  587. package/tests/unit/providers/codex/runtime/codexAppServerTypes.test.ts +336 -0
  588. package/tests/unit/providers/codex/settings.test.ts +189 -0
  589. package/tests/unit/providers/codex/skills/CodexSkillListingService.test.ts +178 -0
  590. package/tests/unit/providers/codex/storage/CodexSkillStorage.test.ts +342 -0
  591. package/tests/unit/providers/codex/storage/CodexSubagentStorage.test.ts +376 -0
  592. package/tests/unit/providers/codex/ui/CodexChatUIConfig.test.ts +211 -0
  593. package/tests/unit/providers/codex/ui/CodexSettingsTab.test.ts +578 -0
  594. package/tests/unit/providers/defaultProviderConfigs.test.ts +18 -0
  595. package/tests/unit/providers/opencode/OpencodeAuxQueryRunner.test.ts +355 -0
  596. package/tests/unit/providers/opencode/OpencodeChatRuntime.test.ts +808 -0
  597. package/tests/unit/providers/opencode/OpencodeCliResolver.test.ts +93 -0
  598. package/tests/unit/providers/opencode/OpencodeCommandCatalog.test.ts +75 -0
  599. package/tests/unit/providers/opencode/OpencodeConversationHistoryService.test.ts +103 -0
  600. package/tests/unit/providers/opencode/OpencodeHistoryStore.test.ts +418 -0
  601. package/tests/unit/providers/opencode/OpencodeLaunchArtifacts.test.ts +268 -0
  602. package/tests/unit/providers/opencode/OpencodePaths.test.ts +35 -0
  603. package/tests/unit/providers/opencode/OpencodeRuntimeCommandLoader.test.ts +129 -0
  604. package/tests/unit/providers/opencode/OpencodeSettingsReconciler.test.ts +96 -0
  605. package/tests/unit/providers/opencode/OpencodeSettingsTab.test.ts +649 -0
  606. package/tests/unit/providers/opencode/OpencodeSqliteReader.test.ts +185 -0
  607. package/tests/unit/providers/opencode/agents/OpencodeAgentMentionProvider.test.ts +56 -0
  608. package/tests/unit/providers/opencode/buildOpencodePrompt.test.ts +87 -0
  609. package/tests/unit/providers/opencode/capabilities.test.ts +39 -0
  610. package/tests/unit/providers/opencode/models.test.ts +273 -0
  611. package/tests/unit/providers/opencode/modes.test.ts +151 -0
  612. package/tests/unit/providers/opencode/opencodeToolNormalization.test.ts +197 -0
  613. package/tests/unit/providers/opencode/settings.test.ts +469 -0
  614. package/tests/unit/providers/opencode/storage/OpencodeAgentStorage.test.ts +377 -0
  615. package/tests/unit/providers/opencode/ui/OpencodeAgentSettings.test.ts +91 -0
  616. package/tests/unit/providers/pi/PiRuntimeCommandLoader.test.ts +149 -0
  617. package/tests/unit/providers/pi/capabilities.test.ts +20 -0
  618. package/tests/unit/providers/pi/commands/PiCommandCatalog.test.ts +69 -0
  619. package/tests/unit/providers/pi/env/PiSettingsReconciler.test.ts +61 -0
  620. package/tests/unit/providers/pi/history/PiConversationHistoryService.test.ts +147 -0
  621. package/tests/unit/providers/pi/history/PiHistoryStore.test.ts +523 -0
  622. package/tests/unit/providers/pi/models.test.ts +96 -0
  623. package/tests/unit/providers/pi/registration.test.ts +54 -0
  624. package/tests/unit/providers/pi/runtime/PiAuxQueryRunner.test.ts +172 -0
  625. package/tests/unit/providers/pi/runtime/PiChatRuntime.test.ts +830 -0
  626. package/tests/unit/providers/pi/runtime/PiCliResolver.test.ts +105 -0
  627. package/tests/unit/providers/pi/runtime/PiEventNormalization.test.ts +147 -0
  628. package/tests/unit/providers/pi/runtime/PiExtensionUiBridge.test.ts +98 -0
  629. package/tests/unit/providers/pi/runtime/PiJsonl.test.ts +42 -0
  630. package/tests/unit/providers/pi/runtime/PiLaunchSpec.test.ts +92 -0
  631. package/tests/unit/providers/pi/runtime/PiModelDiscoveryService.test.ts +135 -0
  632. package/tests/unit/providers/pi/runtime/PiRpcPayloads.test.ts +15 -0
  633. package/tests/unit/providers/pi/runtime/PiRpcTransport.test.ts +116 -0
  634. package/tests/unit/providers/pi/runtime/PiSubprocess.test.ts +151 -0
  635. package/tests/unit/providers/pi/runtime/buildPiPrompt.test.ts +84 -0
  636. package/tests/unit/providers/pi/runtime/buildPiUsageInfo.test.ts +69 -0
  637. package/tests/unit/providers/pi/settings.test.ts +253 -0
  638. package/tests/unit/providers/pi/ui/PiChatUIConfig.test.ts +161 -0
  639. package/tests/unit/providers/pi/ui/PiSettingsTab.test.ts +492 -0
  640. package/tests/unit/scripts/rendererSafeUnref.test.ts +101 -0
  641. package/tests/unit/shared/components/ResumeSessionDropdown.test.ts +356 -0
  642. package/tests/unit/shared/components/SelectableDropdown.test.ts +406 -0
  643. package/tests/unit/shared/components/SlashCommandDropdown.provider.test.ts +354 -0
  644. package/tests/unit/shared/components/SlashCommandDropdown.test.ts +508 -0
  645. package/tests/unit/shared/icons.test.ts +56 -0
  646. package/tests/unit/shared/index.test.ts +46 -0
  647. package/tests/unit/shared/mention/MentionDropdownController.test.ts +823 -0
  648. package/tests/unit/shared/mention/VaultFileCache.test.ts +195 -0
  649. package/tests/unit/shared/mention/VaultFolderCache.test.ts +143 -0
  650. package/tests/unit/shared/mention/VaultMentionDataProvider.test.ts +87 -0
  651. package/tests/unit/shared/modals/ConfirmModal.test.ts +111 -0
  652. package/tests/unit/shared/modals/ForkTargetModal.test.ts +101 -0
  653. package/tests/unit/shared/modals/InstructionConfirmModal.test.ts +305 -0
  654. package/tests/unit/utils/agent.test.ts +395 -0
  655. package/tests/unit/utils/animationFrame.test.ts +59 -0
  656. package/tests/unit/utils/browser.test.ts +73 -0
  657. package/tests/unit/utils/canvas.test.ts +54 -0
  658. package/tests/unit/utils/claudeCli.test.ts +294 -0
  659. package/tests/unit/utils/cliBinaryLocator.test.ts +33 -0
  660. package/tests/unit/utils/context.test.ts +288 -0
  661. package/tests/unit/utils/contextMentionResolver.test.ts +270 -0
  662. package/tests/unit/utils/date.test.ts +80 -0
  663. package/tests/unit/utils/diff.test.ts +291 -0
  664. package/tests/unit/utils/editor.test.ts +249 -0
  665. package/tests/unit/utils/electronCompat.test.ts +87 -0
  666. package/tests/unit/utils/env.test.ts +1240 -0
  667. package/tests/unit/utils/externalContext.test.ts +336 -0
  668. package/tests/unit/utils/externalContextScanner.test.ts +186 -0
  669. package/tests/unit/utils/fileLink.dom.test.ts +273 -0
  670. package/tests/unit/utils/fileLink.handler.test.ts +64 -0
  671. package/tests/unit/utils/fileLink.test.ts +233 -0
  672. package/tests/unit/utils/frontmatter.test.ts +434 -0
  673. package/tests/unit/utils/imageEmbed.test.ts +407 -0
  674. package/tests/unit/utils/inlineEdit.test.ts +63 -0
  675. package/tests/unit/utils/interrupt.test.ts +73 -0
  676. package/tests/unit/utils/markdown.test.ts +35 -0
  677. package/tests/unit/utils/markdownMath.test.ts +54 -0
  678. package/tests/unit/utils/mcp.test.ts +256 -0
  679. package/tests/unit/utils/obsidianCompat.test.ts +18 -0
  680. package/tests/unit/utils/path.test.ts +677 -0
  681. package/tests/unit/utils/sdkSession.test.ts +2359 -0
  682. package/tests/unit/utils/session.test.ts +971 -0
  683. package/tests/unit/utils/slashCommand.test.ts +778 -0
  684. package/tests/unit/utils/utils.test.ts +809 -0
  685. package/tsconfig.jest.json +8 -0
  686. package/tsconfig.json +26 -0
  687. package/versions.json +4 -0
@@ -0,0 +1,1240 @@
1
+ import type * as fsType from 'fs';
2
+ import type * as pathType from 'path';
3
+
4
+ const fs = jest.requireActual<typeof fsType>('fs');
5
+ const path = jest.requireActual<typeof pathType>('path');
6
+
7
+ import * as env from '../../../src/utils/env';
8
+
9
+ const {
10
+ cliPathRequiresNode,
11
+ findNodeDirectory,
12
+ findNodeExecutable,
13
+ formatContextLimit,
14
+ getEnhancedPath,
15
+ getMissingNodeError,
16
+ getHostnameKey,
17
+ migrateLegacyHostnameKeyedMap,
18
+ parseContextLimit,
19
+ parseEnvironmentVariables,
20
+ } = env;
21
+
22
+ const isWindows = process.platform === 'win32';
23
+ const SEP = isWindows ? ';' : ':';
24
+
25
+ describe('parseEnvironmentVariables', () => {
26
+ it('parses simple KEY=VALUE pairs', () => {
27
+ const input = 'FOO=bar\nBAZ=qux';
28
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar', BAZ: 'qux' });
29
+ });
30
+
31
+ it('handles quoted values', () => {
32
+ const input = 'FOO="bar baz"\nQUX=\'hello world\'';
33
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar baz', QUX: 'hello world' });
34
+ });
35
+
36
+ it('ignores comments and empty lines', () => {
37
+ const input = '# comment\nFOO=bar\n\n# another\nBAZ=qux';
38
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar', BAZ: 'qux' });
39
+ });
40
+
41
+ it('handles Windows line endings', () => {
42
+ const input = 'FOO=bar\r\nBAZ=qux\r\n';
43
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar', BAZ: 'qux' });
44
+ });
45
+
46
+ it('handles equals sign in value', () => {
47
+ const input = 'FOO=bar=baz';
48
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar=baz' });
49
+ });
50
+
51
+ it('trims whitespace around keys and values', () => {
52
+ const input = ' FOO = bar ';
53
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar' });
54
+ });
55
+
56
+ it('strips export prefix from shell snippets', () => {
57
+ const input = 'export FOO=bar\nexport BAZ="hello world"';
58
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar', BAZ: 'hello world' });
59
+ });
60
+
61
+ it('handles mixed export and non-export lines', () => {
62
+ const input = 'FOO=bar\nexport BAZ=qux\nQUX=123';
63
+ expect(parseEnvironmentVariables(input)).toEqual({ FOO: 'bar', BAZ: 'qux', QUX: '123' });
64
+ });
65
+ });
66
+
67
+ describe('getEnhancedPath', () => {
68
+ const originalEnv = { ...process.env };
69
+
70
+ afterEach(() => {
71
+ // Restore environment
72
+ Object.keys(process.env).forEach(key => delete process.env[key]);
73
+ Object.assign(process.env, originalEnv);
74
+ });
75
+
76
+ describe('basic functionality', () => {
77
+ it('returns a non-empty string', () => {
78
+ const result = getEnhancedPath();
79
+ expect(typeof result).toBe('string');
80
+ expect(result.length).toBeGreaterThan(0);
81
+ });
82
+
83
+ it('includes current PATH from process.env', () => {
84
+ process.env.PATH = `/existing/path${SEP}/another/path`;
85
+ const result = getEnhancedPath();
86
+ expect(result).toContain('/existing/path');
87
+ expect(result).toContain('/another/path');
88
+ });
89
+
90
+ it('works when process.env.PATH is empty', () => {
91
+ process.env.PATH = '';
92
+ const result = getEnhancedPath();
93
+ expect(typeof result).toBe('string');
94
+ // Should still have extra paths
95
+ expect(result.length).toBeGreaterThan(0);
96
+ });
97
+
98
+ it('works when process.env.PATH is undefined', () => {
99
+ delete process.env.PATH;
100
+ const result = getEnhancedPath();
101
+ expect(typeof result).toBe('string');
102
+ expect(result.length).toBeGreaterThan(0);
103
+ });
104
+ });
105
+
106
+ describe('platform-specific separator', () => {
107
+ it('uses correct separator for current platform', () => {
108
+ const result = getEnhancedPath();
109
+ // Result should contain the platform-specific separator
110
+ expect(result).toContain(SEP);
111
+ });
112
+
113
+ it('splits and joins with platform separator', () => {
114
+ const result = getEnhancedPath();
115
+ const segments = result.split(SEP);
116
+ // Should have multiple segments
117
+ expect(segments.length).toBeGreaterThan(1);
118
+ // Rejoining should give same result
119
+ expect(segments.join(SEP)).toBe(result);
120
+ });
121
+
122
+ it('handles input with platform separator', () => {
123
+ const customPath = `/custom/bin1${SEP}/custom/bin2`;
124
+ const result = getEnhancedPath(customPath);
125
+ expect(result).toContain('/custom/bin1');
126
+ expect(result).toContain('/custom/bin2');
127
+ });
128
+ });
129
+
130
+ describe('custom PATH merging and priority', () => {
131
+ it('prepends additional paths (highest priority)', () => {
132
+ process.env.PATH = '/existing/path';
133
+ const result = getEnhancedPath('/custom/bin');
134
+ const segments = result.split(SEP);
135
+ // Custom path should be first
136
+ expect(segments[0]).toBe('/custom/bin');
137
+ // Existing should come after extra paths
138
+ expect(segments.indexOf('/custom/bin')).toBeLessThan(segments.indexOf('/existing/path'));
139
+ });
140
+
141
+ it('merges multiple additional paths in order', () => {
142
+ const customPath = `/first/bin${SEP}/second/bin${SEP}/third/bin`;
143
+ const result = getEnhancedPath(customPath);
144
+ const segments = result.split(SEP);
145
+ expect(segments[0]).toBe('/first/bin');
146
+ expect(segments[1]).toBe('/second/bin');
147
+ expect(segments[2]).toBe('/third/bin');
148
+ });
149
+
150
+ it('preserves priority: additional > extra > current', () => {
151
+ process.env.PATH = '/usr/bin';
152
+ const result = getEnhancedPath('/user/custom');
153
+ const segments = result.split(SEP);
154
+
155
+ const customIndex = segments.indexOf('/user/custom');
156
+ const usrBinIndex = segments.indexOf('/usr/bin');
157
+
158
+ // Custom should come before current PATH
159
+ expect(customIndex).toBeLessThan(usrBinIndex);
160
+ });
161
+
162
+ it('handles undefined additional paths', () => {
163
+ process.env.PATH = '/existing/path';
164
+ const result = getEnhancedPath(undefined);
165
+ expect(result).toContain('/existing/path');
166
+ });
167
+
168
+ it('handles empty string additional paths', () => {
169
+ process.env.PATH = '/existing/path';
170
+ const result = getEnhancedPath('');
171
+ expect(result).toContain('/existing/path');
172
+ // Should not have empty segments
173
+ const segments = result.split(SEP);
174
+ expect(segments.every(s => s.length > 0)).toBe(true);
175
+ });
176
+ });
177
+
178
+ describe('deduplication logic', () => {
179
+ it('removes duplicate paths', () => {
180
+ process.env.PATH = `/usr/local/bin${SEP}/usr/bin`;
181
+ const result = getEnhancedPath('/usr/local/bin');
182
+ const segments = result.split(SEP);
183
+ const count = segments.filter(s => s === '/usr/local/bin').length;
184
+ expect(count).toBe(1);
185
+ });
186
+
187
+ it('preserves first occurrence when deduplicating', () => {
188
+ // Additional path should win over current PATH
189
+ process.env.PATH = `/duplicate/path${SEP}/other/path`;
190
+ const result = getEnhancedPath('/duplicate/path');
191
+ const segments = result.split(SEP);
192
+ // First occurrence should be from additional paths
193
+ expect(segments[0]).toBe('/duplicate/path');
194
+ });
195
+
196
+ it('deduplicates across all sources', () => {
197
+ // Path appears in additional, might be in extra paths, and in current
198
+ process.env.PATH = `/usr/local/bin${SEP}/usr/bin${SEP}/usr/local/bin`;
199
+ const result = getEnhancedPath(`/usr/local/bin${SEP}/usr/bin`);
200
+ const segments = result.split(SEP);
201
+
202
+ // Each unique path should appear only once
203
+ const localBinCount = segments.filter(s => s === '/usr/local/bin').length;
204
+ const usrBinCount = segments.filter(s => s === '/usr/bin').length;
205
+ expect(localBinCount).toBe(1);
206
+ expect(usrBinCount).toBe(1);
207
+ });
208
+
209
+ // Note: Case-insensitive deduplication on Windows is tested implicitly
210
+ // since the module uses lowercase comparison on win32
211
+ });
212
+
213
+ describe('empty segment filtering', () => {
214
+ it('filters out empty segments from current PATH', () => {
215
+ process.env.PATH = `/usr/bin${SEP}${SEP}/bin${SEP}`;
216
+ const result = getEnhancedPath();
217
+ const segments = result.split(SEP);
218
+ expect(segments.every(s => s.length > 0)).toBe(true);
219
+ });
220
+
221
+ it('filters out empty segments from additional paths', () => {
222
+ const result = getEnhancedPath(`${SEP}/custom/bin${SEP}${SEP}`);
223
+ const segments = result.split(SEP);
224
+ expect(segments.every(s => s.length > 0)).toBe(true);
225
+ });
226
+
227
+ it('handles path with only empty segments', () => {
228
+ process.env.PATH = `${SEP}${SEP}${SEP}`;
229
+ const result = getEnhancedPath(`${SEP}${SEP}`);
230
+ const segments = result.split(SEP);
231
+ expect(segments.every(s => s.length > 0)).toBe(true);
232
+ });
233
+ });
234
+
235
+ describe('extra binary paths', () => {
236
+ it('returns non-empty result with extra paths', () => {
237
+ const result = getEnhancedPath();
238
+ // On both platforms, result should be non-empty
239
+ expect(result.length).toBeGreaterThan(0);
240
+ });
241
+
242
+ it('includes platform-appropriate paths', () => {
243
+ const result = getEnhancedPath();
244
+ const segments = result.split(SEP);
245
+ // Should have added some extra paths beyond just process.env.PATH
246
+ expect(segments.length).toBeGreaterThan(1);
247
+ });
248
+
249
+ it('includes the default OpenCode install bin path from HOME', () => {
250
+ process.env.HOME = '/mock/home';
251
+ const result = getEnhancedPath();
252
+ const segments = result.split(SEP);
253
+
254
+ expect(segments).toContain(path.join('/mock/home', '.opencode', 'bin'));
255
+ });
256
+ });
257
+
258
+ describe('Unix environment variable paths', () => {
259
+ if (isWindows) return;
260
+
261
+ it('includes VOLTA_HOME/bin when set', () => {
262
+ process.env.VOLTA_HOME = '/custom/volta';
263
+ const result = getEnhancedPath();
264
+ expect(result).toContain('/custom/volta/bin');
265
+ delete process.env.VOLTA_HOME;
266
+ });
267
+
268
+ it('includes ASDF_DATA_DIR shims and bin when set', () => {
269
+ process.env.ASDF_DATA_DIR = '/custom/asdf';
270
+ const result = getEnhancedPath();
271
+ expect(result).toContain('/custom/asdf/shims');
272
+ expect(result).toContain('/custom/asdf/bin');
273
+ delete process.env.ASDF_DATA_DIR;
274
+ });
275
+
276
+ it('includes ASDF_DIR shims and bin when set', () => {
277
+ delete process.env.ASDF_DATA_DIR;
278
+ process.env.ASDF_DIR = '/alt/asdf';
279
+ const result = getEnhancedPath();
280
+ expect(result).toContain('/alt/asdf/shims');
281
+ expect(result).toContain('/alt/asdf/bin');
282
+ delete process.env.ASDF_DIR;
283
+ });
284
+
285
+ it('includes FNM_MULTISHELL_PATH when set', () => {
286
+ process.env.FNM_MULTISHELL_PATH = '/tmp/fnm_multishell';
287
+ const result = getEnhancedPath();
288
+ expect(result).toContain('/tmp/fnm_multishell');
289
+ delete process.env.FNM_MULTISHELL_PATH;
290
+ });
291
+
292
+ it('includes FNM_DIR when set', () => {
293
+ process.env.FNM_DIR = '/custom/fnm';
294
+ const result = getEnhancedPath();
295
+ expect(result).toContain('/custom/fnm');
296
+ delete process.env.FNM_DIR;
297
+ });
298
+
299
+ it('includes NVM_BIN when set', () => {
300
+ process.env.NVM_BIN = '/home/user/.nvm/versions/node/v20/bin';
301
+ const result = getEnhancedPath();
302
+ expect(result).toContain('/home/user/.nvm/versions/node/v20/bin');
303
+ delete process.env.NVM_BIN;
304
+ });
305
+
306
+ describe('nvm fallback when NVM_BIN is not set (Unix)', () => {
307
+ if (isWindows) return;
308
+
309
+ let savedNvmBin: string | undefined;
310
+ let savedNvmDir: string | undefined;
311
+ let savedHome: string | undefined;
312
+
313
+ beforeEach(() => {
314
+ savedNvmBin = process.env.NVM_BIN;
315
+ savedNvmDir = process.env.NVM_DIR;
316
+ savedHome = process.env.HOME;
317
+ delete process.env.NVM_BIN;
318
+ delete process.env.NVM_DIR;
319
+ process.env.HOME = '/fake/home';
320
+ });
321
+
322
+ afterEach(() => {
323
+ jest.restoreAllMocks();
324
+ if (savedNvmBin !== undefined) process.env.NVM_BIN = savedNvmBin;
325
+ else delete process.env.NVM_BIN;
326
+ if (savedNvmDir !== undefined) process.env.NVM_DIR = savedNvmDir;
327
+ else delete process.env.NVM_DIR;
328
+ if (savedHome !== undefined) process.env.HOME = savedHome;
329
+ else delete process.env.HOME;
330
+ });
331
+
332
+ function mockNvm(opts: {
333
+ nvmDir: string;
334
+ aliasFiles: Record<string, string>;
335
+ versions: string[];
336
+ existingBinDirs?: string[];
337
+ }) {
338
+ const { nvmDir, aliasFiles, versions, existingBinDirs } = opts;
339
+ const binDirs = existingBinDirs ?? versions.map(v => path.join(nvmDir, 'versions', 'node', v, 'bin'));
340
+
341
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => binDirs.includes(String(p)));
342
+ jest.spyOn(fs, 'readFileSync').mockImplementation(((p: string) => {
343
+ const s = String(p);
344
+ for (const [aliasPath, value] of Object.entries(aliasFiles)) {
345
+ if (s === aliasPath) return value;
346
+ }
347
+ throw new Error('not found');
348
+ }) as typeof fs.readFileSync);
349
+ jest.spyOn(fs, 'readdirSync').mockImplementation(((p: string) => {
350
+ if (String(p) === path.join(nvmDir, 'versions', 'node')) return versions;
351
+ return [];
352
+ }) as typeof fs.readdirSync);
353
+ jest.spyOn(fs, 'statSync').mockImplementation(
354
+ () => ({ isFile: () => true, isDirectory: () => true }) as fsType.Stats
355
+ );
356
+ }
357
+
358
+ it('resolves default version from alias file', () => {
359
+ const nvmDir = '/fake/home/.nvm';
360
+ const versionBin = path.join(nvmDir, 'versions', 'node', 'v22.18.0', 'bin');
361
+ mockNvm({
362
+ nvmDir,
363
+ aliasFiles: { [path.join(nvmDir, 'alias', 'default')]: '22' },
364
+ versions: ['v22.18.0'],
365
+ });
366
+
367
+ expect(getEnhancedPath()).toContain(versionBin);
368
+ });
369
+
370
+ it('respects NVM_DIR env var', () => {
371
+ process.env.NVM_DIR = '/custom/nvm';
372
+ const versionBin = '/custom/nvm/versions/node/v20.10.0/bin';
373
+ mockNvm({
374
+ nvmDir: '/custom/nvm',
375
+ aliasFiles: { '/custom/nvm/alias/default': '20' },
376
+ versions: ['v20.10.0'],
377
+ });
378
+
379
+ expect(getEnhancedPath()).toContain(versionBin);
380
+ });
381
+
382
+ it('picks highest matching version when multiple match', () => {
383
+ const nvmDir = '/fake/home/.nvm';
384
+ const expectedBin = path.join(nvmDir, 'versions', 'node', 'v22.18.0', 'bin');
385
+ mockNvm({
386
+ nvmDir,
387
+ aliasFiles: { [path.join(nvmDir, 'alias', 'default')]: '22' },
388
+ versions: ['v22.5.0', 'v22.18.0', 'v20.10.0'],
389
+ });
390
+
391
+ const result = getEnhancedPath();
392
+ expect(result).toContain(expectedBin);
393
+ expect(result).not.toContain('v22.5.0');
394
+ });
395
+
396
+ it('follows alias chains (lts/* → lts/jod → version)', () => {
397
+ const nvmDir = '/fake/home/.nvm';
398
+ const versionBin = path.join(nvmDir, 'versions', 'node', 'v22.18.0', 'bin');
399
+ mockNvm({
400
+ nvmDir,
401
+ aliasFiles: {
402
+ [path.join(nvmDir, 'alias', 'default')]: 'lts/*',
403
+ [path.join(nvmDir, 'alias', 'lts', '*')]: 'lts/jod',
404
+ [path.join(nvmDir, 'alias', 'lts', 'jod')]: 'v22.18.0',
405
+ },
406
+ versions: ['v22.18.0', 'v20.10.0'],
407
+ });
408
+
409
+ expect(getEnhancedPath()).toContain(versionBin);
410
+ });
411
+
412
+ it.each(['node', 'stable'])(
413
+ 'resolves built-in %s alias to the highest installed version',
414
+ builtInAlias => {
415
+ const nvmDir = '/fake/home/.nvm';
416
+ const expectedBin = path.join(nvmDir, 'versions', 'node', 'v22.18.0', 'bin');
417
+ mockNvm({
418
+ nvmDir,
419
+ aliasFiles: {
420
+ [path.join(nvmDir, 'alias', 'default')]: builtInAlias,
421
+ },
422
+ versions: ['v20.10.0', 'v22.18.0'],
423
+ });
424
+
425
+ const result = getEnhancedPath();
426
+ expect(result).toContain(expectedBin);
427
+ expect(result).not.toContain('v20.10.0');
428
+ }
429
+ );
430
+ });
431
+ });
432
+
433
+ describe('CLI path parameter for Node.js detection', () => {
434
+ afterEach(() => {
435
+ jest.restoreAllMocks();
436
+ });
437
+
438
+ function mockNodeExecutable(fakeDir: string) {
439
+ const nodePath = path.join(fakeDir, isWindows ? 'node.exe' : 'node');
440
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === nodePath);
441
+ jest.spyOn(fs, 'statSync').mockImplementation(
442
+ p => ({ isFile: () => String(p) === nodePath }) as fsType.Stats
443
+ );
444
+ return nodePath;
445
+ }
446
+
447
+ it('prepends detected node directory before extra paths when cliPath is .js', () => {
448
+ const fakeDir = isWindows ? 'C:\\fake\\node' : '/tmp/fake-node';
449
+ mockNodeExecutable(fakeDir);
450
+
451
+ const otherPath = isWindows ? 'C:\\other' : '/other';
452
+ process.env.PATH = `${fakeDir}${SEP}${otherPath}`;
453
+ if (isWindows) {
454
+ process.env.ProgramFiles = 'C:\\Program Files';
455
+ }
456
+
457
+ const result = getEnhancedPath(undefined, '/path/to/cli.js');
458
+ const segments = result.split(SEP);
459
+ const extraPath = isWindows ? 'C:\\Program Files\\nodejs' : '/usr/local/bin';
460
+
461
+ const nodeIndex = segments.indexOf(fakeDir);
462
+ const extraIndex = segments.indexOf(extraPath);
463
+
464
+ expect(nodeIndex).toBeGreaterThanOrEqual(0);
465
+ expect(extraIndex).toBeGreaterThanOrEqual(0);
466
+ expect(nodeIndex).toBeLessThan(extraIndex);
467
+ });
468
+
469
+ it('does not prepend node directory when cliPath is native binary', () => {
470
+ const fakeDir = isWindows ? 'C:\\fake\\node' : '/tmp/fake-node';
471
+ mockNodeExecutable(fakeDir);
472
+
473
+ const otherPath = isWindows ? 'C:\\other' : '/other';
474
+ process.env.PATH = `${fakeDir}${SEP}${otherPath}`;
475
+ if (isWindows) {
476
+ process.env.ProgramFiles = 'C:\\Program Files';
477
+ }
478
+
479
+ const result = getEnhancedPath(undefined, '/path/to/claude.exe');
480
+ const segments = result.split(SEP);
481
+ const extraPath = isWindows ? 'C:\\Program Files\\nodejs' : '/usr/local/bin';
482
+
483
+ const nodeIndex = segments.indexOf(fakeDir);
484
+ const extraIndex = segments.indexOf(extraPath);
485
+
486
+ expect(nodeIndex).toBeGreaterThanOrEqual(0);
487
+ expect(extraIndex).toBeGreaterThanOrEqual(0);
488
+ expect(nodeIndex).toBeGreaterThan(extraIndex);
489
+ });
490
+
491
+ it('accepts cliPath parameter without error', () => {
492
+ const result = getEnhancedPath(undefined, '/path/to/cli.js');
493
+ expect(typeof result).toBe('string');
494
+ expect(result.length).toBeGreaterThan(0);
495
+ });
496
+
497
+ it('works with both additionalPaths and cliPath', () => {
498
+ const result = getEnhancedPath('/custom/path', '/path/to/cli.js');
499
+ expect(result).toContain('/custom/path');
500
+ });
501
+
502
+ it('works with native binary path (no Node.js detection needed)', () => {
503
+ const result = getEnhancedPath(undefined, '/path/to/claude.exe');
504
+ expect(typeof result).toBe('string');
505
+ });
506
+ });
507
+
508
+ describe('CLI directory with node executable (nvm/fnm/volta/asdf support)', () => {
509
+ afterEach(() => {
510
+ jest.restoreAllMocks();
511
+ });
512
+
513
+ function mockCliDirWithNode(cliDir: string) {
514
+ const nodePath = path.join(cliDir, isWindows ? 'node.exe' : 'node');
515
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === nodePath);
516
+ jest.spyOn(fs, 'statSync').mockImplementation(
517
+ p => ({ isFile: () => String(p) === nodePath }) as fsType.Stats
518
+ );
519
+ }
520
+
521
+ it('adds CLI directory to PATH when it contains node (Unix nvm)', () => {
522
+ if (isWindows) return;
523
+
524
+ const nvmBinDir = '/Users/test/.nvm/versions/node/v20.10.0/bin';
525
+ const cliPath = path.join(nvmBinDir, 'claude');
526
+ mockCliDirWithNode(nvmBinDir);
527
+
528
+ process.env.PATH = '/usr/bin';
529
+ const result = getEnhancedPath(undefined, cliPath);
530
+ const segments = result.split(SEP);
531
+
532
+ // CLI directory should be added and come before /usr/bin
533
+ expect(segments).toContain(nvmBinDir);
534
+ expect(segments.indexOf(nvmBinDir)).toBeLessThan(segments.indexOf('/usr/bin'));
535
+ });
536
+
537
+ it('adds CLI directory to PATH when it contains node (Windows nvm)', () => {
538
+ if (!isWindows) return;
539
+
540
+ const nvmBinDir = 'C:\\Users\\test\\AppData\\Roaming\\nvm\\v20.10.0';
541
+ const cliPath = path.join(nvmBinDir, 'claude.cmd');
542
+ mockCliDirWithNode(nvmBinDir);
543
+
544
+ process.env.PATH = 'C:\\Windows\\System32';
545
+ const result = getEnhancedPath(undefined, cliPath);
546
+ const segments = result.split(SEP);
547
+
548
+ // CLI directory should be added (case-insensitive check for Windows)
549
+ const hasNvmDir = segments.some(s => s.toLowerCase() === nvmBinDir.toLowerCase());
550
+ expect(hasNvmDir).toBe(true);
551
+ });
552
+
553
+ it('adds CLI directory to PATH for fnm installation', () => {
554
+ if (isWindows) return;
555
+
556
+ const fnmBinDir = '/Users/test/.fnm/node-versions/v20.10.0/installation/bin';
557
+ const cliPath = path.join(fnmBinDir, 'claude');
558
+ mockCliDirWithNode(fnmBinDir);
559
+
560
+ process.env.PATH = '/usr/bin';
561
+ const result = getEnhancedPath(undefined, cliPath);
562
+
563
+ expect(result).toContain(fnmBinDir);
564
+ });
565
+
566
+ it('adds CLI directory to PATH for volta installation', () => {
567
+ if (isWindows) return;
568
+
569
+ const voltaBinDir = '/Users/test/.volta/bin';
570
+ const cliPath = path.join(voltaBinDir, 'claude');
571
+ mockCliDirWithNode(voltaBinDir);
572
+
573
+ process.env.PATH = '/usr/bin';
574
+ const result = getEnhancedPath(undefined, cliPath);
575
+
576
+ expect(result).toContain(voltaBinDir);
577
+ });
578
+
579
+ it('adds CLI directory to PATH for asdf installation', () => {
580
+ if (isWindows) return;
581
+
582
+ const asdfBinDir = '/Users/test/.asdf/installs/nodejs/20.10.0/bin';
583
+ const cliPath = path.join(asdfBinDir, 'claude');
584
+ mockCliDirWithNode(asdfBinDir);
585
+
586
+ process.env.PATH = '/usr/bin';
587
+ const result = getEnhancedPath(undefined, cliPath);
588
+
589
+ expect(result).toContain(asdfBinDir);
590
+ });
591
+
592
+ it('does not add CLI directory when node is not present', () => {
593
+ const cliDir = isWindows ? 'C:\\custom\\bin' : '/custom/bin';
594
+ const cliPath = path.join(cliDir, isWindows ? 'claude.exe' : 'claude');
595
+
596
+ // Mock: node does not exist in CLI directory
597
+ jest.spyOn(fs, 'existsSync').mockReturnValue(false);
598
+
599
+ process.env.PATH = isWindows ? 'C:\\Windows\\System32' : '/usr/bin';
600
+ const result = getEnhancedPath(undefined, cliPath);
601
+
602
+ expect(result).not.toContain(cliDir);
603
+ });
604
+
605
+ it('CLI directory has higher priority than fallback node search', () => {
606
+ if (isWindows) return;
607
+
608
+ const nvmBinDir = '/Users/test/.nvm/versions/node/v20.10.0/bin';
609
+ const cliPath = path.join(nvmBinDir, 'cli.js'); // JS file
610
+
611
+ // Mock: node exists in CLI directory
612
+ const nodePath = path.join(nvmBinDir, 'node');
613
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === nodePath);
614
+ jest.spyOn(fs, 'statSync').mockImplementation(
615
+ p => ({ isFile: () => String(p) === nodePath }) as fsType.Stats
616
+ );
617
+
618
+ process.env.PATH = '/usr/bin';
619
+ const result = getEnhancedPath(undefined, cliPath);
620
+ const segments = result.split(SEP);
621
+
622
+ // CLI directory should be first (after any additional paths)
623
+ expect(segments[0]).toBe(nvmBinDir);
624
+ });
625
+
626
+ it('user additional paths have highest priority over CLI directory', () => {
627
+ if (isWindows) return;
628
+
629
+ const nvmBinDir = '/Users/test/.nvm/versions/node/v20.10.0/bin';
630
+ const cliPath = path.join(nvmBinDir, 'claude');
631
+ mockCliDirWithNode(nvmBinDir);
632
+
633
+ const userPath = '/user/custom/bin';
634
+ process.env.PATH = '/usr/bin';
635
+ const result = getEnhancedPath(userPath, cliPath);
636
+ const segments = result.split(SEP);
637
+
638
+ // User path should be first, then CLI directory
639
+ expect(segments[0]).toBe(userPath);
640
+ expect(segments[1]).toBe(nvmBinDir);
641
+ });
642
+ });
643
+ });
644
+
645
+ describe('cliPathRequiresNode', () => {
646
+ afterEach(() => {
647
+ jest.restoreAllMocks();
648
+ });
649
+
650
+ it('returns true for .js files', () => {
651
+ expect(cliPathRequiresNode('/path/to/cli.js')).toBe(true);
652
+ expect(cliPathRequiresNode('C:\\path\\to\\cli.js')).toBe(true);
653
+ });
654
+
655
+ it('returns true for other JS extensions', () => {
656
+ expect(cliPathRequiresNode('/path/to/cli.mjs')).toBe(true);
657
+ expect(cliPathRequiresNode('/path/to/cli.cjs')).toBe(true);
658
+ expect(cliPathRequiresNode('/path/to/cli.ts')).toBe(true);
659
+ expect(cliPathRequiresNode('/path/to/cli.tsx')).toBe(true);
660
+ expect(cliPathRequiresNode('/path/to/cli.jsx')).toBe(true);
661
+ });
662
+
663
+ it('returns false for native binaries', () => {
664
+ expect(cliPathRequiresNode('/path/to/claude')).toBe(false);
665
+ expect(cliPathRequiresNode('/path/to/claude.exe')).toBe(false);
666
+ expect(cliPathRequiresNode('C:\\path\\to\\claude.exe')).toBe(false);
667
+ });
668
+
669
+ it('returns true for scripts with node shebang', () => {
670
+ const scriptPath = isWindows ? 'C:\\temp\\claude' : '/tmp/claude';
671
+ const shebang = '#!/usr/bin/env node\nconsole.log("hi");\n';
672
+
673
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === scriptPath);
674
+ jest.spyOn(fs, 'statSync').mockImplementation(
675
+ p => ({ isFile: () => String(p) === scriptPath }) as fsType.Stats
676
+ );
677
+ jest.spyOn(fs, 'openSync').mockImplementation(() => 1 as any);
678
+ jest.spyOn(fs, 'readSync').mockImplementation((_, buffer: ArrayBufferView) => {
679
+ Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength).write(shebang);
680
+ return shebang.length;
681
+ });
682
+ jest.spyOn(fs, 'closeSync').mockImplementation(() => {});
683
+
684
+ expect(cliPathRequiresNode(scriptPath)).toBe(true);
685
+ });
686
+
687
+ it('returns false when path exists but is a directory', () => {
688
+ const dirPath = isWindows ? 'C:\\temp\\claude' : '/tmp/claude';
689
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === dirPath);
690
+ jest.spyOn(fs, 'statSync').mockImplementation(
691
+ () => ({ isFile: () => false }) as fsType.Stats
692
+ );
693
+
694
+ expect(cliPathRequiresNode(dirPath)).toBe(false);
695
+ });
696
+
697
+ it('returns false for scripts without node shebang', () => {
698
+ const scriptPath = isWindows ? 'C:\\temp\\script' : '/tmp/script';
699
+ const shebang = '#!/usr/bin/env python\nprint("hi")\n';
700
+
701
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === scriptPath);
702
+ jest.spyOn(fs, 'statSync').mockImplementation(
703
+ p => ({ isFile: () => String(p) === scriptPath }) as fsType.Stats
704
+ );
705
+ jest.spyOn(fs, 'openSync').mockImplementation(() => 1 as any);
706
+ jest.spyOn(fs, 'readSync').mockImplementation((_, buffer: ArrayBufferView) => {
707
+ Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength).write(shebang);
708
+ return shebang.length;
709
+ });
710
+ jest.spyOn(fs, 'closeSync').mockImplementation(() => {});
711
+
712
+ expect(cliPathRequiresNode(scriptPath)).toBe(false);
713
+ });
714
+
715
+ it('returns false for non-node shebang scripts that reference node in the body', () => {
716
+ const scriptPath = isWindows ? 'C:\\temp\\claude-wrapper' : '/tmp/claude-wrapper';
717
+ const script = [
718
+ '#!/usr/bin/env bash',
719
+ 'set -euo pipefail',
720
+ 'NODE_BIN="$(command -v node)"',
721
+ 'exec "$NODE_BIN" /path/to/cli.js "$@"',
722
+ '',
723
+ ].join('\n');
724
+
725
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === scriptPath);
726
+ jest.spyOn(fs, 'statSync').mockImplementation(
727
+ p => ({ isFile: () => String(p) === scriptPath }) as fsType.Stats
728
+ );
729
+ jest.spyOn(fs, 'openSync').mockImplementation(() => 1 as any);
730
+ jest.spyOn(fs, 'readSync').mockImplementation((_, buffer: ArrayBufferView) => {
731
+ Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength).write(script);
732
+ return script.length;
733
+ });
734
+ jest.spyOn(fs, 'closeSync').mockImplementation(() => {});
735
+
736
+ expect(cliPathRequiresNode(scriptPath)).toBe(false);
737
+ });
738
+
739
+ it('returns false for .cmd files', () => {
740
+ expect(cliPathRequiresNode('/path/to/claude.cmd')).toBe(false);
741
+ });
742
+
743
+ it('is case-insensitive', () => {
744
+ expect(cliPathRequiresNode('/path/to/CLI.JS')).toBe(true);
745
+ expect(cliPathRequiresNode('/path/to/cli.MJS')).toBe(true);
746
+ expect(cliPathRequiresNode('/path/to/cli-wrapper.CJS')).toBe(true);
747
+ });
748
+ });
749
+
750
+ describe('getMissingNodeError', () => {
751
+ afterEach(() => {
752
+ jest.restoreAllMocks();
753
+ });
754
+
755
+ it('returns null when CLI does not require Node.js', () => {
756
+ jest.spyOn(fs, 'existsSync').mockReturnValue(false);
757
+ const error = getMissingNodeError('/path/to/claude');
758
+ expect(error).toBeNull();
759
+ });
760
+
761
+ it('returns error when Node.js is missing and CLI requires Node.js', () => {
762
+ jest.spyOn(fs, 'existsSync').mockReturnValue(false);
763
+ const error = getMissingNodeError('/path/to/cli.js', '/missing');
764
+ expect(error).toContain('Node.js');
765
+ });
766
+
767
+ it('returns null when Node.js is found on PATH', () => {
768
+ const nodeDir = isWindows ? 'C:\\custom\\bin' : '/custom/bin';
769
+ const nodePath = path.join(nodeDir, isWindows ? 'node.exe' : 'node');
770
+
771
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === nodePath);
772
+ jest.spyOn(fs, 'statSync').mockImplementation(
773
+ p => ({ isFile: () => String(p) === nodePath }) as fsType.Stats
774
+ );
775
+
776
+ const error = getMissingNodeError('/path/to/cli.js', nodeDir);
777
+ expect(error).toBeNull();
778
+ });
779
+ });
780
+
781
+ describe('findNodeDirectory', () => {
782
+ const originalEnv = { ...process.env };
783
+
784
+ afterEach(() => {
785
+ jest.restoreAllMocks();
786
+ Object.keys(process.env).forEach(key => delete process.env[key]);
787
+ Object.assign(process.env, originalEnv);
788
+ });
789
+
790
+ it('returns string or null', () => {
791
+ const result = findNodeDirectory();
792
+ expect(result === null || typeof result === 'string').toBe(true);
793
+ });
794
+
795
+ it('returns a non-empty string when node is found', () => {
796
+ const result = findNodeDirectory();
797
+ // On most dev machines, node should be findable
798
+ // Result is either null (not found) or a non-empty directory path
799
+ const isValidResult = result === null || (typeof result === 'string' && result.length > 0);
800
+ expect(isValidResult).toBe(true);
801
+ });
802
+
803
+ it('uses NVM_SYMLINK when set on Windows', () => {
804
+ if (!isWindows) {
805
+ return;
806
+ }
807
+
808
+ const nvmSymlink = 'C:\\nvm\\symlink';
809
+ const nodePath = path.join(nvmSymlink, 'node.exe');
810
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === nodePath);
811
+ jest.spyOn(fs, 'statSync').mockImplementation(
812
+ p => ({ isFile: () => String(p) === nodePath }) as fsType.Stats
813
+ );
814
+
815
+ process.env.NVM_SYMLINK = nvmSymlink;
816
+ process.env.PATH = '';
817
+
818
+ const result = findNodeDirectory();
819
+ expect(result).toBe(nvmSymlink);
820
+ });
821
+
822
+ it('prefers additionalPaths over process.env.PATH', () => {
823
+ const nodeExecutable = isWindows ? 'node.exe' : 'node';
824
+ const preferredDir = isWindows ? 'C:\\custom\\bin' : '/custom/bin';
825
+ const fallbackDir = isWindows ? 'C:\\fallback\\bin' : '/fallback/bin';
826
+ const preferredNode = path.join(preferredDir, nodeExecutable);
827
+ const fallbackNode = path.join(fallbackDir, nodeExecutable);
828
+
829
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => {
830
+ const candidate = String(p);
831
+ return candidate === preferredNode || candidate === fallbackNode;
832
+ });
833
+ jest.spyOn(fs, 'statSync').mockImplementation(
834
+ () => ({ isFile: () => true }) as fsType.Stats
835
+ );
836
+
837
+ process.env.PATH = fallbackDir;
838
+
839
+ const result = findNodeDirectory(preferredDir);
840
+ expect(result).toBe(preferredDir);
841
+ });
842
+
843
+ it('returns full path for findNodeExecutable when available', () => {
844
+ const nodeExecutable = isWindows ? 'node.exe' : 'node';
845
+ const preferredDir = isWindows ? 'C:\\custom\\bin' : '/custom/bin';
846
+ const preferredNode = path.join(preferredDir, nodeExecutable);
847
+
848
+ jest.spyOn(fs, 'existsSync').mockImplementation(p => String(p) === preferredNode);
849
+ jest.spyOn(fs, 'statSync').mockImplementation(
850
+ () => ({ isFile: () => true }) as fsType.Stats
851
+ );
852
+
853
+ const result = findNodeExecutable(preferredDir);
854
+ expect(result).toBe(preferredNode);
855
+ });
856
+ });
857
+
858
+ describe('getHostnameKey', () => {
859
+ it('returns a non-empty string', () => {
860
+ const key = getHostnameKey();
861
+ expect(typeof key).toBe('string');
862
+ expect(key.length).toBeGreaterThan(0);
863
+ });
864
+
865
+ it('returns an opaque device key instead of the system hostname', () => {
866
+ const key = getHostnameKey();
867
+ expect(key).toMatch(/^device:/);
868
+ });
869
+
870
+ it('returns consistent value on repeated calls', () => {
871
+ const first = getHostnameKey();
872
+ const second = getHostnameKey();
873
+ expect(first).toBe(second);
874
+ });
875
+
876
+ it('migrates the current legacy hostname entry to the opaque device key', () => {
877
+ const migrated = migrateLegacyHostnameKeyedMap(
878
+ {
879
+ 'legacy-host': '/legacy/cli',
880
+ 'other-host': '/other/cli',
881
+ },
882
+ 'device:new',
883
+ 'legacy-host',
884
+ );
885
+
886
+ expect(migrated).toEqual({
887
+ 'device:new': '/legacy/cli',
888
+ 'other-host': '/other/cli',
889
+ });
890
+ });
891
+ });
892
+
893
+ describe('parseContextLimit', () => {
894
+ it('should parse "256k" to 256000', () => {
895
+ expect(parseContextLimit('256k')).toBe(256000);
896
+ });
897
+
898
+ it('should parse "256K" to 256000 (case insensitive)', () => {
899
+ expect(parseContextLimit('256K')).toBe(256000);
900
+ });
901
+
902
+ it('should parse "1m" to 1000000', () => {
903
+ expect(parseContextLimit('1m')).toBe(1000000);
904
+ });
905
+
906
+ it('should parse "1M" to 1000000 (case insensitive)', () => {
907
+ expect(parseContextLimit('1M')).toBe(1000000);
908
+ });
909
+
910
+ it('should parse "1000000" to 1000000', () => {
911
+ expect(parseContextLimit('1000000')).toBe(1000000);
912
+ });
913
+
914
+ it('should parse "1.5m" to 1500000', () => {
915
+ expect(parseContextLimit('1.5m')).toBe(1500000);
916
+ });
917
+
918
+ it('should parse "200k" to 200000', () => {
919
+ expect(parseContextLimit('200k')).toBe(200000);
920
+ });
921
+
922
+ it('should handle whitespace', () => {
923
+ expect(parseContextLimit(' 256k ')).toBe(256000);
924
+ });
925
+
926
+ it('should handle space before suffix', () => {
927
+ expect(parseContextLimit('256 k')).toBe(256000);
928
+ expect(parseContextLimit('1 m')).toBe(1000000);
929
+ expect(parseContextLimit('1.5 m')).toBe(1500000);
930
+ });
931
+
932
+ it('should return null for empty string', () => {
933
+ expect(parseContextLimit('')).toBeNull();
934
+ });
935
+
936
+ it('should return null for whitespace only', () => {
937
+ expect(parseContextLimit(' ')).toBeNull();
938
+ });
939
+
940
+ it('should return null for invalid input', () => {
941
+ expect(parseContextLimit('abc')).toBeNull();
942
+ expect(parseContextLimit('k256')).toBeNull();
943
+ expect(parseContextLimit('256x')).toBeNull();
944
+ });
945
+
946
+ it('should return null for negative values', () => {
947
+ expect(parseContextLimit('-100k')).toBeNull();
948
+ });
949
+
950
+ it('should return null for zero', () => {
951
+ expect(parseContextLimit('0k')).toBeNull();
952
+ });
953
+
954
+ it('should return null for values below 1k', () => {
955
+ expect(parseContextLimit('100')).toBeNull();
956
+ expect(parseContextLimit('999')).toBeNull();
957
+ });
958
+
959
+ it('should return null for values above 10m', () => {
960
+ expect(parseContextLimit('20m')).toBeNull();
961
+ expect(parseContextLimit('11000000')).toBeNull();
962
+ });
963
+
964
+ it('should accept boundary values', () => {
965
+ expect(parseContextLimit('1k')).toBe(1000);
966
+ expect(parseContextLimit('10m')).toBe(10000000);
967
+ });
968
+ });
969
+
970
+ describe('formatContextLimit', () => {
971
+ it('should format 256000 as "256k"', () => {
972
+ expect(formatContextLimit(256000)).toBe('256k');
973
+ });
974
+
975
+ it('should format 1000000 as "1m"', () => {
976
+ expect(formatContextLimit(1000000)).toBe('1m');
977
+ });
978
+
979
+ it('should format 200000 as "200k"', () => {
980
+ expect(formatContextLimit(200000)).toBe('200k');
981
+ });
982
+
983
+ it('should format 2000000 as "2m"', () => {
984
+ expect(formatContextLimit(2000000)).toBe('2m');
985
+ });
986
+
987
+ it('should format non-round numbers with toLocaleString', () => {
988
+ expect(formatContextLimit(256500)).toBe('256,500');
989
+ });
990
+
991
+ it('should format small numbers with toLocaleString', () => {
992
+ expect(formatContextLimit(500)).toBe('500');
993
+ });
994
+
995
+ it('should round-trip through parseContextLimit for all formats', () => {
996
+ // Round numbers (k/m suffix)
997
+ expect(parseContextLimit(formatContextLimit(256000))).toBe(256000);
998
+ expect(parseContextLimit(formatContextLimit(1000000))).toBe(1000000);
999
+ expect(parseContextLimit(formatContextLimit(2000000))).toBe(2000000);
1000
+
1001
+ // Non-round numbers (locale-formatted with commas)
1002
+ expect(parseContextLimit(formatContextLimit(256500))).toBe(256500);
1003
+ expect(parseContextLimit(formatContextLimit(1234567))).toBe(1234567);
1004
+ });
1005
+ });
1006
+
1007
+ describe('parseContextLimit with comma-formatted input', () => {
1008
+ it('should parse "256,500" to 256500', () => {
1009
+ expect(parseContextLimit('256,500')).toBe(256500);
1010
+ });
1011
+
1012
+ it('should parse "1,000,000" to 1000000', () => {
1013
+ expect(parseContextLimit('1,000,000')).toBe(1000000);
1014
+ });
1015
+
1016
+ it('should parse "1,234,567" to 1234567', () => {
1017
+ expect(parseContextLimit('1,234,567')).toBe(1234567);
1018
+ });
1019
+ });
1020
+
1021
+
1022
+ describe('getExtraBinaryPaths (Windows branches)', () => {
1023
+ const originalPlatform = process.platform;
1024
+ const originalEnv = { ...process.env };
1025
+
1026
+ afterEach(() => {
1027
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
1028
+ Object.keys(process.env).forEach(key => delete process.env[key]);
1029
+ Object.assign(process.env, originalEnv);
1030
+ jest.resetModules();
1031
+ });
1032
+
1033
+ function loadWithWindowsPlatform(): typeof env {
1034
+ jest.resetModules();
1035
+ Object.defineProperty(process, 'platform', { value: 'win32', writable: true });
1036
+ // Dynamic require needed to re-evaluate module with mocked platform
1037
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1038
+ return require('../../../src/utils/env');
1039
+ }
1040
+
1041
+ it('includes APPDATA npm path when APPDATA is set', () => {
1042
+ process.env.APPDATA = '/mock/AppData/Roaming';
1043
+ process.env.LOCALAPPDATA = '/mock/AppData/Local';
1044
+ process.env.HOME = '/mock/home';
1045
+ const mod = loadWithWindowsPlatform();
1046
+ const result = mod.getEnhancedPath();
1047
+ expect(result).toContain('npm');
1048
+ expect(result).toContain('Roaming');
1049
+ });
1050
+
1051
+ it('includes LOCALAPPDATA nodejs paths', () => {
1052
+ process.env.LOCALAPPDATA = '/mock/AppData/Local';
1053
+ process.env.HOME = '/mock/home';
1054
+ const mod = loadWithWindowsPlatform();
1055
+ const result = mod.getEnhancedPath();
1056
+ expect(result).toContain('Local');
1057
+ expect(result).toContain('nodejs');
1058
+ });
1059
+
1060
+ it('includes NVM_SYMLINK when set', () => {
1061
+ process.env.NVM_SYMLINK = '/mock/nvm/symlink';
1062
+ process.env.HOME = '/mock/home';
1063
+ const mod = loadWithWindowsPlatform();
1064
+ const result = mod.getEnhancedPath();
1065
+ expect(result).toContain('/mock/nvm/symlink');
1066
+ });
1067
+
1068
+ it('includes NVM_HOME when set', () => {
1069
+ process.env.NVM_HOME = '/mock/nvm/home';
1070
+ process.env.HOME = '/mock/home';
1071
+ const mod = loadWithWindowsPlatform();
1072
+ const result = mod.getEnhancedPath();
1073
+ expect(result).toContain('/mock/nvm/home');
1074
+ });
1075
+
1076
+ it('falls back to APPDATA/nvm when NVM_HOME not set', () => {
1077
+ delete process.env.NVM_HOME;
1078
+ process.env.APPDATA = '/mock/AppData/Roaming';
1079
+ process.env.HOME = '/mock/home';
1080
+ const mod = loadWithWindowsPlatform();
1081
+ const result = mod.getEnhancedPath();
1082
+ expect(result).toContain('nvm');
1083
+ });
1084
+
1085
+ it('includes VOLTA_HOME/bin when set', () => {
1086
+ process.env.VOLTA_HOME = '/mock/volta';
1087
+ process.env.HOME = '/mock/home';
1088
+ const mod = loadWithWindowsPlatform();
1089
+ const result = mod.getEnhancedPath();
1090
+ expect(result).toContain('volta');
1091
+ });
1092
+
1093
+ it('falls back to home/.volta/bin when VOLTA_HOME not set', () => {
1094
+ delete process.env.VOLTA_HOME;
1095
+ process.env.HOME = '/mock/home';
1096
+ const mod = loadWithWindowsPlatform();
1097
+ const result = mod.getEnhancedPath();
1098
+ expect(result).toContain('.volta');
1099
+ });
1100
+
1101
+ it('includes FNM_MULTISHELL_PATH when set', () => {
1102
+ process.env.FNM_MULTISHELL_PATH = '/mock/fnm/multishell';
1103
+ process.env.HOME = '/mock/home';
1104
+ const mod = loadWithWindowsPlatform();
1105
+ const result = mod.getEnhancedPath();
1106
+ expect(result).toContain('/mock/fnm/multishell');
1107
+ });
1108
+
1109
+ it('includes FNM_DIR when set', () => {
1110
+ process.env.FNM_DIR = '/mock/fnm/dir';
1111
+ process.env.HOME = '/mock/home';
1112
+ const mod = loadWithWindowsPlatform();
1113
+ const result = mod.getEnhancedPath();
1114
+ expect(result).toContain('/mock/fnm/dir');
1115
+ });
1116
+
1117
+ it('falls back to LOCALAPPDATA/fnm when FNM_DIR not set', () => {
1118
+ delete process.env.FNM_DIR;
1119
+ process.env.LOCALAPPDATA = '/mock/AppData/Local';
1120
+ process.env.HOME = '/mock/home';
1121
+ const mod = loadWithWindowsPlatform();
1122
+ const result = mod.getEnhancedPath();
1123
+ expect(result).toContain('fnm');
1124
+ });
1125
+
1126
+ it('includes ChocolateyInstall/bin when set', () => {
1127
+ process.env.ChocolateyInstall = '/mock/choco';
1128
+ process.env.HOME = '/mock/home';
1129
+ const mod = loadWithWindowsPlatform();
1130
+ const result = mod.getEnhancedPath();
1131
+ expect(result).toContain('choco');
1132
+ });
1133
+
1134
+ it('falls back to ProgramData/chocolatey/bin when ChocolateyInstall not set', () => {
1135
+ delete process.env.ChocolateyInstall;
1136
+ process.env.HOME = '/mock/home';
1137
+ const mod = loadWithWindowsPlatform();
1138
+ const result = mod.getEnhancedPath();
1139
+ expect(result).toContain('chocolatey');
1140
+ });
1141
+
1142
+ it('includes SCOOP paths when set', () => {
1143
+ process.env.SCOOP = '/mock/scoop';
1144
+ process.env.HOME = '/mock/home';
1145
+ const mod = loadWithWindowsPlatform();
1146
+ const result = mod.getEnhancedPath();
1147
+ expect(result).toContain('scoop');
1148
+ expect(result).toContain('shims');
1149
+ });
1150
+
1151
+ it('falls back to home/scoop when SCOOP not set', () => {
1152
+ delete process.env.SCOOP;
1153
+ process.env.HOME = '/mock/home';
1154
+ const mod = loadWithWindowsPlatform();
1155
+ const result = mod.getEnhancedPath();
1156
+ expect(result).toContain('scoop');
1157
+ });
1158
+
1159
+ it('includes Docker path under Program Files', () => {
1160
+ process.env.ProgramFiles = '/mock/Program Files';
1161
+ process.env.HOME = '/mock/home';
1162
+ const mod = loadWithWindowsPlatform();
1163
+ const result = mod.getEnhancedPath();
1164
+ expect(result).toContain('Docker');
1165
+ });
1166
+
1167
+ it('includes home/.local/bin on Windows', () => {
1168
+ process.env.HOME = '/mock/home';
1169
+ const mod = loadWithWindowsPlatform();
1170
+ const result = mod.getEnhancedPath();
1171
+ expect(result).toContain('.local');
1172
+ });
1173
+
1174
+ it('includes home/.opencode/bin on Windows', () => {
1175
+ process.env.HOME = '/mock/home';
1176
+ const mod = loadWithWindowsPlatform();
1177
+ const result = mod.getEnhancedPath();
1178
+ const segments = result.split(';');
1179
+
1180
+ expect(segments).toContain(path.join('/mock/home', '.opencode', 'bin'));
1181
+ });
1182
+
1183
+ it('uses semicolon separator on Windows', () => {
1184
+ process.env.HOME = '/mock/home';
1185
+ process.env.PATH = '/existing';
1186
+ const mod = loadWithWindowsPlatform();
1187
+ const result = mod.getEnhancedPath();
1188
+ expect(result).toContain(';');
1189
+ });
1190
+ });
1191
+
1192
+ describe('Obsidian CLI path integration', () => {
1193
+ const originalPlatform = process.platform;
1194
+ const originalExecPath = process.execPath;
1195
+ const originalEnv = { ...process.env };
1196
+
1197
+ afterEach(() => {
1198
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
1199
+ Object.defineProperty(process, 'execPath', { value: originalExecPath });
1200
+ Object.keys(process.env).forEach(key => delete process.env[key]);
1201
+ Object.assign(process.env, originalEnv);
1202
+ jest.resetModules();
1203
+ });
1204
+
1205
+ function loadWithPlatform(platform: NodeJS.Platform, execPath: string): typeof env {
1206
+ jest.resetModules();
1207
+ Object.defineProperty(process, 'platform', { value: platform, writable: true });
1208
+ Object.defineProperty(process, 'execPath', { value: execPath, configurable: true });
1209
+ // Dynamic require needed to re-evaluate module with mocked platform/execPath
1210
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1211
+ return require('../../../src/utils/env');
1212
+ }
1213
+
1214
+ it('uses the top-level app bundle binary dir on macOS helper processes', () => {
1215
+ const helperExecPath = '/Applications/Obsidian.app/Contents/Frameworks/Obsidian Helper (Renderer).app/Contents/MacOS/Obsidian Helper (Renderer)';
1216
+ process.env.PATH = '';
1217
+
1218
+ const mod = loadWithPlatform('darwin', helperExecPath);
1219
+ const result = mod.getEnhancedPath();
1220
+ const segments = result.split(':');
1221
+
1222
+ expect(segments).toContain('/Applications/Obsidian.app/Contents/MacOS');
1223
+ expect(segments).not.toContain('/Applications/Obsidian.app/Contents/Frameworks/Obsidian Helper (Renderer).app/Contents/MacOS');
1224
+ });
1225
+
1226
+ it('does not add transient Linux AppImage mount dirs', () => {
1227
+ const appImageExecPath = '/tmp/.mount_Obsidian-abcd1234/usr/bin/obsidian';
1228
+ const appImageDir = path.dirname(appImageExecPath);
1229
+ process.env.HOME = '/home/test';
1230
+ process.env.PATH = '';
1231
+
1232
+ const mod = loadWithPlatform('linux', appImageExecPath);
1233
+ const result = mod.getEnhancedPath();
1234
+ const segments = result.split(':');
1235
+
1236
+ expect(segments).not.toContain(appImageDir);
1237
+ expect(segments).toContain('/usr/local/bin');
1238
+ expect(segments).toContain('/home/test/.local/bin');
1239
+ });
1240
+ });