@butlerw/vellum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/__fixtures__/responses/code-generation.json +42 -0
  4. package/__fixtures__/responses/error-response.json +20 -0
  5. package/__fixtures__/responses/hello-world.json +32 -0
  6. package/dist/auth-6MCXESOH.js +26 -0
  7. package/dist/chunk-SECXJGWA.js +597 -0
  8. package/dist/index.js +34023 -0
  9. package/package.json +67 -0
  10. package/src/__tests__/commands.e2e.test.ts +728 -0
  11. package/src/__tests__/credentials.test.ts +713 -0
  12. package/src/__tests__/mode-e2e.test.ts +391 -0
  13. package/src/__tests__/tui-integration.test.tsx +1271 -0
  14. package/src/agents/__tests__/task-persistence.test.ts +235 -0
  15. package/src/agents/commands/delegate.ts +240 -0
  16. package/src/agents/commands/index.ts +10 -0
  17. package/src/agents/commands/resume.ts +335 -0
  18. package/src/agents/index.ts +29 -0
  19. package/src/agents/task-persistence.ts +272 -0
  20. package/src/agents/task-resumption.ts +242 -0
  21. package/src/app.tsx +4737 -0
  22. package/src/commands/__tests__/.gitkeep +1 -0
  23. package/src/commands/__tests__/agents.test.ts +606 -0
  24. package/src/commands/__tests__/auth.test.ts +626 -0
  25. package/src/commands/__tests__/autocomplete.test.ts +683 -0
  26. package/src/commands/__tests__/batch.test.ts +287 -0
  27. package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
  28. package/src/commands/__tests__/completion.test.ts +238 -0
  29. package/src/commands/__tests__/core.test.ts +363 -0
  30. package/src/commands/__tests__/executor.test.ts +496 -0
  31. package/src/commands/__tests__/exit-codes.test.ts +220 -0
  32. package/src/commands/__tests__/init.test.ts +243 -0
  33. package/src/commands/__tests__/language.test.ts +353 -0
  34. package/src/commands/__tests__/mode-cli.test.ts +667 -0
  35. package/src/commands/__tests__/model.test.ts +277 -0
  36. package/src/commands/__tests__/parser.test.ts +493 -0
  37. package/src/commands/__tests__/performance.bench.ts +380 -0
  38. package/src/commands/__tests__/registry.test.ts +534 -0
  39. package/src/commands/__tests__/resume.test.ts +449 -0
  40. package/src/commands/__tests__/security.test.ts +845 -0
  41. package/src/commands/__tests__/stream-json.test.ts +372 -0
  42. package/src/commands/__tests__/user-commands.test.ts +597 -0
  43. package/src/commands/adapters.ts +267 -0
  44. package/src/commands/agent.ts +395 -0
  45. package/src/commands/agents/generate.ts +506 -0
  46. package/src/commands/agents/index.ts +272 -0
  47. package/src/commands/agents/show.ts +271 -0
  48. package/src/commands/agents/validate.ts +387 -0
  49. package/src/commands/auth.ts +883 -0
  50. package/src/commands/autocomplete.ts +480 -0
  51. package/src/commands/batch/command.ts +388 -0
  52. package/src/commands/batch/executor.ts +361 -0
  53. package/src/commands/batch/index.ts +12 -0
  54. package/src/commands/commit.ts +235 -0
  55. package/src/commands/completion/index.ts +371 -0
  56. package/src/commands/condense.ts +191 -0
  57. package/src/commands/config.ts +344 -0
  58. package/src/commands/context-provider.ts +173 -0
  59. package/src/commands/copy.ts +329 -0
  60. package/src/commands/core/clear.ts +38 -0
  61. package/src/commands/core/exit.ts +43 -0
  62. package/src/commands/core/help.ts +354 -0
  63. package/src/commands/core/index.ts +15 -0
  64. package/src/commands/cost.ts +179 -0
  65. package/src/commands/credentials.tsx +618 -0
  66. package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
  67. package/src/commands/custom-agents/create.ts +377 -0
  68. package/src/commands/custom-agents/export.ts +135 -0
  69. package/src/commands/custom-agents/import.ts +199 -0
  70. package/src/commands/custom-agents/index.ts +372 -0
  71. package/src/commands/custom-agents/info.ts +318 -0
  72. package/src/commands/custom-agents/list.ts +267 -0
  73. package/src/commands/custom-agents/validate.ts +388 -0
  74. package/src/commands/diff-mode.ts +241 -0
  75. package/src/commands/env.ts +53 -0
  76. package/src/commands/executor.ts +579 -0
  77. package/src/commands/exit-codes.ts +202 -0
  78. package/src/commands/index.ts +701 -0
  79. package/src/commands/init/index.ts +15 -0
  80. package/src/commands/init/prompts.ts +366 -0
  81. package/src/commands/init/templates/commands-readme.md +80 -0
  82. package/src/commands/init/templates/example-command.md +79 -0
  83. package/src/commands/init/templates/example-skill.md +168 -0
  84. package/src/commands/init/templates/example-workflow.md +101 -0
  85. package/src/commands/init/templates/prompts-readme.md +52 -0
  86. package/src/commands/init/templates/rules-readme.md +63 -0
  87. package/src/commands/init/templates/skills-readme.md +83 -0
  88. package/src/commands/init/templates/workflows-readme.md +94 -0
  89. package/src/commands/init.ts +391 -0
  90. package/src/commands/install.ts +90 -0
  91. package/src/commands/language.ts +191 -0
  92. package/src/commands/loaders/.gitkeep +1 -0
  93. package/src/commands/lsp.ts +199 -0
  94. package/src/commands/markdown-commands.ts +253 -0
  95. package/src/commands/mcp.ts +588 -0
  96. package/src/commands/memory/export.ts +341 -0
  97. package/src/commands/memory/index.ts +148 -0
  98. package/src/commands/memory/list.ts +261 -0
  99. package/src/commands/memory/search.ts +346 -0
  100. package/src/commands/memory/utils.ts +15 -0
  101. package/src/commands/metrics.ts +75 -0
  102. package/src/commands/migrate/index.ts +16 -0
  103. package/src/commands/migrate/prompts.ts +477 -0
  104. package/src/commands/mode.ts +331 -0
  105. package/src/commands/model.ts +298 -0
  106. package/src/commands/onboard.ts +205 -0
  107. package/src/commands/open.ts +169 -0
  108. package/src/commands/output/stream-json.ts +373 -0
  109. package/src/commands/parser/chain-parser.ts +370 -0
  110. package/src/commands/parser/index.ts +29 -0
  111. package/src/commands/parser/pipe-parser.ts +480 -0
  112. package/src/commands/parser.ts +588 -0
  113. package/src/commands/persistence.ts +355 -0
  114. package/src/commands/progress.ts +18 -0
  115. package/src/commands/prompt/index.ts +17 -0
  116. package/src/commands/prompt/validate.ts +621 -0
  117. package/src/commands/prompt-priority.ts +401 -0
  118. package/src/commands/registry.ts +374 -0
  119. package/src/commands/sandbox/index.ts +131 -0
  120. package/src/commands/security/index.ts +21 -0
  121. package/src/commands/security/input-sanitizer.ts +168 -0
  122. package/src/commands/security/permission-checker.ts +456 -0
  123. package/src/commands/security/sensitive-data.ts +350 -0
  124. package/src/commands/session/delete.ts +38 -0
  125. package/src/commands/session/export.ts +39 -0
  126. package/src/commands/session/index.ts +26 -0
  127. package/src/commands/session/list.ts +26 -0
  128. package/src/commands/session/resume.ts +562 -0
  129. package/src/commands/session/search.ts +434 -0
  130. package/src/commands/session/show.ts +26 -0
  131. package/src/commands/settings.ts +368 -0
  132. package/src/commands/setup.ts +23 -0
  133. package/src/commands/shell/index.ts +16 -0
  134. package/src/commands/shell/setup.ts +422 -0
  135. package/src/commands/shell-init.ts +50 -0
  136. package/src/commands/shell-integration/index.ts +194 -0
  137. package/src/commands/skill.ts +1220 -0
  138. package/src/commands/spec.ts +558 -0
  139. package/src/commands/status.ts +246 -0
  140. package/src/commands/theme.ts +211 -0
  141. package/src/commands/think.ts +551 -0
  142. package/src/commands/trust.ts +211 -0
  143. package/src/commands/tutorial.ts +522 -0
  144. package/src/commands/types.ts +512 -0
  145. package/src/commands/update.ts +274 -0
  146. package/src/commands/usage.ts +213 -0
  147. package/src/commands/user-commands.ts +630 -0
  148. package/src/commands/utils.ts +142 -0
  149. package/src/commands/vim.ts +152 -0
  150. package/src/commands/workflow.ts +257 -0
  151. package/src/components/header.tsx +25 -0
  152. package/src/components/input.tsx +25 -0
  153. package/src/components/message-list.tsx +32 -0
  154. package/src/components/status-bar.tsx +23 -0
  155. package/src/index.tsx +614 -0
  156. package/src/onboarding/__tests__/tutorial.test.ts +740 -0
  157. package/src/onboarding/index.ts +69 -0
  158. package/src/onboarding/tips/index.ts +9 -0
  159. package/src/onboarding/tips/tip-engine.ts +459 -0
  160. package/src/onboarding/tutorial/index.ts +88 -0
  161. package/src/onboarding/tutorial/lessons/basics.ts +151 -0
  162. package/src/onboarding/tutorial/lessons/index.ts +151 -0
  163. package/src/onboarding/tutorial/lessons/modes.ts +230 -0
  164. package/src/onboarding/tutorial/lessons/tools.ts +172 -0
  165. package/src/onboarding/tutorial/progress-tracker.ts +350 -0
  166. package/src/onboarding/tutorial/storage.ts +249 -0
  167. package/src/onboarding/tutorial/tutorial-system.ts +462 -0
  168. package/src/onboarding/tutorial/types.ts +310 -0
  169. package/src/orchestrator-singleton.ts +129 -0
  170. package/src/shutdown.ts +33 -0
  171. package/src/test/e2e/assertions.ts +267 -0
  172. package/src/test/e2e/fixtures.ts +204 -0
  173. package/src/test/e2e/harness.ts +575 -0
  174. package/src/test/e2e/index.ts +57 -0
  175. package/src/test/e2e/types.ts +228 -0
  176. package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
  177. package/src/test/fixtures/fake-response-loader.ts +314 -0
  178. package/src/test/fixtures/index.ts +20 -0
  179. package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
  180. package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
  181. package/src/tui/__tests__/mode-components.test.tsx +395 -0
  182. package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
  183. package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
  184. package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
  185. package/src/tui/adapters/agent-adapter.ts +1008 -0
  186. package/src/tui/adapters/index.ts +48 -0
  187. package/src/tui/adapters/message-adapter.ts +315 -0
  188. package/src/tui/adapters/persistence-bridge.ts +331 -0
  189. package/src/tui/adapters/session-adapter.ts +419 -0
  190. package/src/tui/buffered-stdout.ts +223 -0
  191. package/src/tui/components/AgentProgress.tsx +424 -0
  192. package/src/tui/components/Banner/AsciiArt.ts +160 -0
  193. package/src/tui/components/Banner/Banner.tsx +355 -0
  194. package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
  195. package/src/tui/components/Banner/ShimmerText.tsx +193 -0
  196. package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
  197. package/src/tui/components/Banner/index.ts +61 -0
  198. package/src/tui/components/Banner/useShimmer.ts +241 -0
  199. package/src/tui/components/ChatView.tsx +11 -0
  200. package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
  201. package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
  202. package/src/tui/components/Checkpoint/index.ts +19 -0
  203. package/src/tui/components/CostDisplay.tsx +226 -0
  204. package/src/tui/components/InitErrorBanner.tsx +122 -0
  205. package/src/tui/components/Input/Autocomplete.tsx +603 -0
  206. package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
  207. package/src/tui/components/Input/HighlightedText.tsx +236 -0
  208. package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
  209. package/src/tui/components/Input/TextInput.tsx +1002 -0
  210. package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
  211. package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
  212. package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
  213. package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
  214. package/src/tui/components/Input/highlight.ts +362 -0
  215. package/src/tui/components/Input/index.ts +36 -0
  216. package/src/tui/components/Input/slash-command-utils.ts +135 -0
  217. package/src/tui/components/Layout.tsx +432 -0
  218. package/src/tui/components/McpPanel.tsx +137 -0
  219. package/src/tui/components/MemoryPanel.tsx +448 -0
  220. package/src/tui/components/Messages/CodeBlock.tsx +527 -0
  221. package/src/tui/components/Messages/DiffView.tsx +679 -0
  222. package/src/tui/components/Messages/ImageReference.tsx +89 -0
  223. package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
  224. package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
  225. package/src/tui/components/Messages/MessageBubble.tsx +270 -0
  226. package/src/tui/components/Messages/MessageList.tsx +1719 -0
  227. package/src/tui/components/Messages/StreamingText.tsx +216 -0
  228. package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
  229. package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
  230. package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
  231. package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
  232. package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
  233. package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
  234. package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
  235. package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
  236. package/src/tui/components/Messages/index.ts +25 -0
  237. package/src/tui/components/ModeIndicator.tsx +177 -0
  238. package/src/tui/components/ModeSelector.tsx +216 -0
  239. package/src/tui/components/ModelSelector.tsx +339 -0
  240. package/src/tui/components/OnboardingWizard.tsx +670 -0
  241. package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
  242. package/src/tui/components/RateLimitIndicator.tsx +82 -0
  243. package/src/tui/components/ScreenReaderLayout.tsx +295 -0
  244. package/src/tui/components/SettingsPanel.tsx +643 -0
  245. package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
  246. package/src/tui/components/Sidebar/index.ts +9 -0
  247. package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
  248. package/src/tui/components/Status/index.ts +12 -0
  249. package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
  250. package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
  251. package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
  252. package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
  253. package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
  254. package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
  255. package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
  256. package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
  257. package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
  258. package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
  259. package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
  260. package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
  261. package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
  262. package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
  263. package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
  264. package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
  265. package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
  266. package/src/tui/components/StatusBar/index.ts +60 -0
  267. package/src/tui/components/TipBanner.tsx +115 -0
  268. package/src/tui/components/TodoItem.tsx +208 -0
  269. package/src/tui/components/TodoPanel.tsx +455 -0
  270. package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
  271. package/src/tui/components/Tools/OptionSelector.tsx +160 -0
  272. package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
  273. package/src/tui/components/Tools/ToolParams.tsx +483 -0
  274. package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
  275. package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
  276. package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
  277. package/src/tui/components/Tools/index.ts +21 -0
  278. package/src/tui/components/TrustPrompt.tsx +279 -0
  279. package/src/tui/components/UpdateBanner.tsx +166 -0
  280. package/src/tui/components/VimModeIndicator.tsx +112 -0
  281. package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
  282. package/src/tui/components/backtrack/index.ts +13 -0
  283. package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
  284. package/src/tui/components/common/CostWarning.tsx +294 -0
  285. package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
  286. package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
  287. package/src/tui/components/common/ErrorBoundary.tsx +140 -0
  288. package/src/tui/components/common/GradientText.tsx +224 -0
  289. package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
  290. package/src/tui/components/common/HotkeyHints.tsx +70 -0
  291. package/src/tui/components/common/MaxSizedBox.tsx +354 -0
  292. package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
  293. package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
  294. package/src/tui/components/common/ScrollIndicator.tsx +160 -0
  295. package/src/tui/components/common/Spinner.tsx +342 -0
  296. package/src/tui/components/common/StreamingIndicator.tsx +316 -0
  297. package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
  298. package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
  299. package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
  300. package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
  301. package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
  302. package/src/tui/components/common/VirtualizedList/index.ts +30 -0
  303. package/src/tui/components/common/VirtualizedList/types.ts +107 -0
  304. package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
  305. package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
  306. package/src/tui/components/common/index.ts +110 -0
  307. package/src/tui/components/index.ts +79 -0
  308. package/src/tui/components/session/CheckpointPanel.tsx +323 -0
  309. package/src/tui/components/session/RollbackDialog.tsx +169 -0
  310. package/src/tui/components/session/SessionItem.tsx +136 -0
  311. package/src/tui/components/session/SessionListPanel.tsx +252 -0
  312. package/src/tui/components/session/SessionPicker.tsx +449 -0
  313. package/src/tui/components/session/SessionPreview.tsx +240 -0
  314. package/src/tui/components/session/__tests__/session.test.tsx +408 -0
  315. package/src/tui/components/session/index.ts +28 -0
  316. package/src/tui/components/session/types.ts +116 -0
  317. package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
  318. package/src/tui/components/theme/index.ts +227 -0
  319. package/src/tui/components/theme/tokens.ts +484 -0
  320. package/src/tui/config/defaults.ts +134 -0
  321. package/src/tui/config/index.ts +17 -0
  322. package/src/tui/context/AnimationContext.tsx +284 -0
  323. package/src/tui/context/AppContext.tsx +349 -0
  324. package/src/tui/context/BracketedPasteContext.tsx +372 -0
  325. package/src/tui/context/LspContext.tsx +192 -0
  326. package/src/tui/context/McpContext.tsx +325 -0
  327. package/src/tui/context/MessagesContext.tsx +870 -0
  328. package/src/tui/context/OverflowContext.tsx +213 -0
  329. package/src/tui/context/RateLimitContext.tsx +108 -0
  330. package/src/tui/context/ResilienceContext.tsx +275 -0
  331. package/src/tui/context/RootProvider.tsx +136 -0
  332. package/src/tui/context/ScrollContext.tsx +331 -0
  333. package/src/tui/context/ToolsContext.tsx +702 -0
  334. package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
  335. package/src/tui/context/index.ts +140 -0
  336. package/src/tui/enterprise-integration.ts +282 -0
  337. package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
  338. package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
  339. package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
  340. package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
  341. package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
  342. package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
  343. package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
  344. package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
  345. package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
  346. package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
  347. package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
  348. package/src/tui/hooks/index.ts +252 -0
  349. package/src/tui/hooks/useAgentLoop.ts +712 -0
  350. package/src/tui/hooks/useAlternateBuffer.ts +398 -0
  351. package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
  352. package/src/tui/hooks/useBacktrack.ts +443 -0
  353. package/src/tui/hooks/useBracketedPaste.ts +104 -0
  354. package/src/tui/hooks/useCollapsible.ts +240 -0
  355. package/src/tui/hooks/useCopyMode.ts +382 -0
  356. package/src/tui/hooks/useCostSummary.ts +75 -0
  357. package/src/tui/hooks/useDesktopNotification.ts +414 -0
  358. package/src/tui/hooks/useDiffMode.ts +44 -0
  359. package/src/tui/hooks/useFileChangeStats.ts +110 -0
  360. package/src/tui/hooks/useFileSuggestions.ts +284 -0
  361. package/src/tui/hooks/useFlickerDetector.ts +250 -0
  362. package/src/tui/hooks/useGitStatus.ts +200 -0
  363. package/src/tui/hooks/useHotkeys.ts +579 -0
  364. package/src/tui/hooks/useImagePaste.ts +114 -0
  365. package/src/tui/hooks/useInputHighlight.ts +145 -0
  366. package/src/tui/hooks/useInputHistory.ts +246 -0
  367. package/src/tui/hooks/useKeyboardScroll.ts +209 -0
  368. package/src/tui/hooks/useLineBuffer.ts +356 -0
  369. package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
  370. package/src/tui/hooks/useModeController.ts +167 -0
  371. package/src/tui/hooks/useModeShortcuts.ts +196 -0
  372. package/src/tui/hooks/usePermissionHandler.ts +146 -0
  373. package/src/tui/hooks/usePersistence.ts +480 -0
  374. package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
  375. package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
  376. package/src/tui/hooks/useProviderStatus.ts +270 -0
  377. package/src/tui/hooks/useRateLimitStatus.ts +90 -0
  378. package/src/tui/hooks/useScreenReader.ts +315 -0
  379. package/src/tui/hooks/useScrollController.ts +450 -0
  380. package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
  381. package/src/tui/hooks/useSidebarPanelData.ts +115 -0
  382. package/src/tui/hooks/useSmoothScroll.ts +202 -0
  383. package/src/tui/hooks/useSnapshots.ts +300 -0
  384. package/src/tui/hooks/useStateAndRef.ts +50 -0
  385. package/src/tui/hooks/useTerminalSize.ts +206 -0
  386. package/src/tui/hooks/useToolApprovalController.ts +91 -0
  387. package/src/tui/hooks/useVim.ts +334 -0
  388. package/src/tui/hooks/useWorkspace.ts +56 -0
  389. package/src/tui/i18n/__tests__/init.test.ts +278 -0
  390. package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
  391. package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
  392. package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
  393. package/src/tui/i18n/index.ts +72 -0
  394. package/src/tui/i18n/init.ts +131 -0
  395. package/src/tui/i18n/language-config.ts +106 -0
  396. package/src/tui/i18n/locale-detection.ts +173 -0
  397. package/src/tui/i18n/settings-integration.ts +557 -0
  398. package/src/tui/i18n/tui-namespace.ts +538 -0
  399. package/src/tui/i18n/types.ts +312 -0
  400. package/src/tui/index.ts +43 -0
  401. package/src/tui/lsp-integration.ts +409 -0
  402. package/src/tui/metrics-integration.ts +366 -0
  403. package/src/tui/plugins.ts +383 -0
  404. package/src/tui/resilience.ts +342 -0
  405. package/src/tui/sandbox-integration.ts +317 -0
  406. package/src/tui/services/clipboard.ts +348 -0
  407. package/src/tui/services/fuzzy-search.ts +441 -0
  408. package/src/tui/services/index.ts +72 -0
  409. package/src/tui/services/markdown-renderer.ts +565 -0
  410. package/src/tui/services/open-external.ts +247 -0
  411. package/src/tui/services/syntax-highlighter.ts +483 -0
  412. package/src/tui/slash-commands.ts +12 -0
  413. package/src/tui/theme/index.ts +15 -0
  414. package/src/tui/theme/provider.tsx +206 -0
  415. package/src/tui/tip-integration.ts +300 -0
  416. package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
  417. package/src/tui/types/ink-extended.ts +87 -0
  418. package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
  419. package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
  420. package/src/tui/utils/__tests__/text-width.test.ts +158 -0
  421. package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
  422. package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
  423. package/src/tui/utils/bracketedPaste.ts +107 -0
  424. package/src/tui/utils/cursor-manager.ts +131 -0
  425. package/src/tui/utils/detectTerminal.ts +596 -0
  426. package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
  427. package/src/tui/utils/heightEstimator.ts +198 -0
  428. package/src/tui/utils/index.ts +91 -0
  429. package/src/tui/utils/isNarrowWidth.ts +52 -0
  430. package/src/tui/utils/stdoutGuard.ts +90 -0
  431. package/src/tui/utils/synchronized-update.ts +70 -0
  432. package/src/tui/utils/text-width.ts +225 -0
  433. package/src/tui/utils/textSanitizer.ts +225 -0
  434. package/src/tui/utils/textUtils.ts +114 -0
  435. package/src/tui/utils/ui-sizing.ts +192 -0
  436. package/src/tui-blessed/app.ts +160 -0
  437. package/src/tui-blessed/index.ts +2 -0
  438. package/src/tui-blessed/neo-blessed.d.ts +6 -0
  439. package/src/tui-blessed/test.ts +21 -0
  440. package/src/tui-blessed/types.ts +14 -0
  441. package/src/utils/icons.ts +130 -0
  442. package/src/utils/index.ts +33 -0
  443. package/src/utils/resume-hint.ts +86 -0
  444. package/src/version.ts +1 -0
  445. package/tsconfig.json +8 -0
  446. package/vitest.config.ts +35 -0
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Chain and Pipe Parser Unit Tests
3
+ *
4
+ * Tests for command chain and pipe parsing including:
5
+ * - Chain operators (&&, ||, ;)
6
+ * - Pipe operators (|, >, >>)
7
+ * - Quote handling
8
+ * - Execution semantics
9
+ *
10
+ * @module cli/commands/__tests__/chain-pipe-parser
11
+ */
12
+
13
+ import { describe, expect, it, vi } from "vitest";
14
+
15
+ import {
16
+ ChainedCommandExecutor,
17
+ ChainParser,
18
+ PipedCommandExecutor,
19
+ PipeParser,
20
+ } from "../parser/index.js";
21
+ import type { CommandResult } from "../types.js";
22
+
23
+ // =============================================================================
24
+ // T053: Chain Parser Tests
25
+ // =============================================================================
26
+
27
+ describe("ChainParser", () => {
28
+ describe("parse", () => {
29
+ it("should parse single command without operators", () => {
30
+ const result = ChainParser.parse("/help");
31
+
32
+ expect(result.isChained).toBe(false);
33
+ expect(result.segments).toHaveLength(1);
34
+ expect(result.segments[0]).toMatchObject({
35
+ command: "/help",
36
+ });
37
+ });
38
+
39
+ it("should parse empty input", () => {
40
+ const result = ChainParser.parse("");
41
+
42
+ expect(result.isChained).toBe(false);
43
+ expect(result.segments).toHaveLength(0);
44
+ });
45
+
46
+ it("should parse AND operator (&&)", () => {
47
+ const result = ChainParser.parse("/build && /test");
48
+
49
+ expect(result.isChained).toBe(true);
50
+ expect(result.segments).toHaveLength(2);
51
+ expect(result.segments[0]).toMatchObject({
52
+ command: "/build",
53
+ operator: "&&",
54
+ });
55
+ expect(result.segments[1]).toMatchObject({
56
+ command: "/test",
57
+ });
58
+ });
59
+
60
+ it("should parse OR operator (||)", () => {
61
+ const result = ChainParser.parse("/check || /fallback");
62
+
63
+ expect(result.isChained).toBe(true);
64
+ expect(result.segments).toHaveLength(2);
65
+ expect(result.segments[0]).toMatchObject({
66
+ command: "/check",
67
+ operator: "||",
68
+ });
69
+ expect(result.segments[1]).toMatchObject({
70
+ command: "/fallback",
71
+ });
72
+ });
73
+
74
+ it("should parse SEQUENCE operator (;)", () => {
75
+ const result = ChainParser.parse("/cmd1 ; /cmd2");
76
+
77
+ expect(result.isChained).toBe(true);
78
+ expect(result.segments).toHaveLength(2);
79
+ expect(result.segments[0]).toMatchObject({
80
+ command: "/cmd1",
81
+ operator: ";",
82
+ });
83
+ expect(result.segments[1]).toMatchObject({
84
+ command: "/cmd2",
85
+ });
86
+ });
87
+
88
+ it("should parse multiple operators", () => {
89
+ const result = ChainParser.parse("/build && /test || /rollback ; /cleanup");
90
+
91
+ expect(result.isChained).toBe(true);
92
+ expect(result.segments).toHaveLength(4);
93
+ expect(result.segments[0]).toMatchObject({ command: "/build", operator: "&&" });
94
+ expect(result.segments[1]).toMatchObject({ command: "/test", operator: "||" });
95
+ expect(result.segments[2]).toMatchObject({ command: "/rollback", operator: ";" });
96
+ expect(result.segments[3]).toMatchObject({ command: "/cleanup" });
97
+ });
98
+
99
+ it("should NOT split on && inside double quotes", () => {
100
+ const result = ChainParser.parse('/echo "foo && bar"');
101
+
102
+ expect(result.isChained).toBe(false);
103
+ expect(result.segments).toHaveLength(1);
104
+ expect(result.segments[0]?.command).toBe('/echo "foo && bar"');
105
+ });
106
+
107
+ it("should NOT split on || inside single quotes", () => {
108
+ const result = ChainParser.parse("/echo 'foo || bar'");
109
+
110
+ expect(result.isChained).toBe(false);
111
+ expect(result.segments).toHaveLength(1);
112
+ expect(result.segments[0]?.command).toBe("/echo 'foo || bar'");
113
+ });
114
+
115
+ it("should NOT split on ; inside quotes", () => {
116
+ const result = ChainParser.parse('/echo "a; b"');
117
+
118
+ expect(result.isChained).toBe(false);
119
+ expect(result.segments).toHaveLength(1);
120
+ });
121
+
122
+ it("should preserve command arguments", () => {
123
+ const result = ChainParser.parse("/cmd1 arg1 --flag && /cmd2 arg2");
124
+
125
+ expect(result.segments[0]?.command).toBe("/cmd1 arg1 --flag");
126
+ expect(result.segments[1]?.command).toBe("/cmd2 arg2");
127
+ });
128
+ });
129
+
130
+ describe("hasChainOperators", () => {
131
+ it("should return true for &&", () => {
132
+ expect(ChainParser.hasChainOperators("/a && /b")).toBe(true);
133
+ });
134
+
135
+ it("should return true for ||", () => {
136
+ expect(ChainParser.hasChainOperators("/a || /b")).toBe(true);
137
+ });
138
+
139
+ it("should return true for ;", () => {
140
+ expect(ChainParser.hasChainOperators("/a ; /b")).toBe(true);
141
+ });
142
+
143
+ it("should return false for single command", () => {
144
+ expect(ChainParser.hasChainOperators("/help")).toBe(false);
145
+ });
146
+
147
+ it("should return false for quoted operators", () => {
148
+ expect(ChainParser.hasChainOperators('/echo "&&"')).toBe(false);
149
+ });
150
+ });
151
+ });
152
+
153
+ describe("ChainedCommandExecutor", () => {
154
+ describe("execute", () => {
155
+ it("should execute single command", async () => {
156
+ const executeFn = vi.fn().mockResolvedValue({
157
+ kind: "success",
158
+ message: "OK",
159
+ } satisfies CommandResult);
160
+
161
+ const executor = new ChainedCommandExecutor(executeFn);
162
+ const result = await executor.execute("/help");
163
+
164
+ expect(executeFn).toHaveBeenCalledTimes(1);
165
+ expect(executeFn).toHaveBeenCalledWith("/help", undefined);
166
+ expect(result.executedCount).toBe(1);
167
+ expect(result.completed).toBe(true);
168
+ });
169
+
170
+ it("should continue after && when previous succeeds", async () => {
171
+ const executeFn = vi.fn().mockResolvedValue({
172
+ kind: "success",
173
+ message: "OK",
174
+ } satisfies CommandResult);
175
+
176
+ const executor = new ChainedCommandExecutor(executeFn);
177
+ const result = await executor.execute("/cmd1 && /cmd2");
178
+
179
+ expect(executeFn).toHaveBeenCalledTimes(2);
180
+ expect(result.executedCount).toBe(2);
181
+ expect(result.completed).toBe(true);
182
+ });
183
+
184
+ it("should STOP after && when previous fails", async () => {
185
+ const executeFn = vi
186
+ .fn()
187
+ .mockResolvedValueOnce({
188
+ kind: "error",
189
+ code: "INTERNAL_ERROR",
190
+ message: "Failed",
191
+ } satisfies CommandResult)
192
+ .mockResolvedValueOnce({
193
+ kind: "success",
194
+ message: "Should not run",
195
+ } satisfies CommandResult);
196
+
197
+ const executor = new ChainedCommandExecutor(executeFn);
198
+ const result = await executor.execute("/cmd1 && /cmd2");
199
+
200
+ expect(executeFn).toHaveBeenCalledTimes(1);
201
+ expect(result.executedCount).toBe(1);
202
+ expect(result.completed).toBe(false);
203
+ });
204
+
205
+ it("should continue after || when previous fails", async () => {
206
+ const executeFn = vi
207
+ .fn()
208
+ .mockResolvedValueOnce({
209
+ kind: "error",
210
+ code: "INTERNAL_ERROR",
211
+ message: "Failed",
212
+ } satisfies CommandResult)
213
+ .mockResolvedValueOnce({
214
+ kind: "success",
215
+ message: "OK",
216
+ } satisfies CommandResult);
217
+
218
+ const executor = new ChainedCommandExecutor(executeFn);
219
+ const result = await executor.execute("/cmd1 || /cmd2");
220
+
221
+ expect(executeFn).toHaveBeenCalledTimes(2);
222
+ expect(result.executedCount).toBe(2);
223
+ expect(result.completed).toBe(true);
224
+ });
225
+
226
+ it("should SKIP after || when previous succeeds", async () => {
227
+ const executeFn = vi
228
+ .fn()
229
+ .mockResolvedValueOnce({
230
+ kind: "success",
231
+ message: "OK",
232
+ } satisfies CommandResult)
233
+ .mockResolvedValueOnce({
234
+ kind: "success",
235
+ message: "Should not run",
236
+ } satisfies CommandResult);
237
+
238
+ const executor = new ChainedCommandExecutor(executeFn);
239
+ const result = await executor.execute("/cmd1 || /cmd2");
240
+
241
+ expect(executeFn).toHaveBeenCalledTimes(1);
242
+ expect(result.executedCount).toBe(1);
243
+ // Note: completed is based on lastExecutedIndex vs total
244
+ });
245
+
246
+ it("should ALWAYS continue after ; regardless of result", async () => {
247
+ const executeFn = vi
248
+ .fn()
249
+ .mockResolvedValueOnce({
250
+ kind: "error",
251
+ code: "INTERNAL_ERROR",
252
+ message: "Failed",
253
+ } satisfies CommandResult)
254
+ .mockResolvedValueOnce({
255
+ kind: "success",
256
+ message: "OK",
257
+ } satisfies CommandResult);
258
+
259
+ const executor = new ChainedCommandExecutor(executeFn);
260
+ const result = await executor.execute("/cmd1 ; /cmd2");
261
+
262
+ expect(executeFn).toHaveBeenCalledTimes(2);
263
+ expect(result.executedCount).toBe(2);
264
+ expect(result.completed).toBe(true);
265
+ });
266
+
267
+ it("should handle abort signal", async () => {
268
+ const controller = new AbortController();
269
+ controller.abort();
270
+
271
+ const executeFn = vi.fn();
272
+ const executor = new ChainedCommandExecutor(executeFn);
273
+ const result = await executor.execute("/cmd1 && /cmd2", controller.signal);
274
+
275
+ expect(executeFn).not.toHaveBeenCalled();
276
+ expect(result.result.kind).toBe("error");
277
+ expect(result.completed).toBe(false);
278
+ });
279
+
280
+ it("should execute complex chain correctly", async () => {
281
+ // /build && /test || /notify ; /cleanup
282
+ // build succeeds -> test runs
283
+ // test fails -> notify runs (|| condition met)
284
+ // cleanup always runs
285
+ const executeFn = vi
286
+ .fn()
287
+ .mockResolvedValueOnce({ kind: "success", message: "Build OK" }) // /build
288
+ .mockResolvedValueOnce({
289
+ kind: "error",
290
+ code: "INTERNAL_ERROR",
291
+ message: "Test failed",
292
+ }) // /test
293
+ .mockResolvedValueOnce({ kind: "success", message: "Notified" }) // /notify
294
+ .mockResolvedValueOnce({ kind: "success", message: "Cleaned" }); // /cleanup
295
+
296
+ const executor = new ChainedCommandExecutor(executeFn);
297
+ const result = await executor.execute("/build && /test || /notify ; /cleanup");
298
+
299
+ expect(executeFn).toHaveBeenCalledTimes(4);
300
+ expect(result.executedCount).toBe(4);
301
+ expect(result.completed).toBe(true);
302
+ });
303
+ });
304
+
305
+ describe("shouldExecute", () => {
306
+ it("should return true for first command", () => {
307
+ expect(ChainedCommandExecutor.shouldExecute(true, undefined)).toBe(true);
308
+ expect(ChainedCommandExecutor.shouldExecute(false, undefined)).toBe(true);
309
+ });
310
+
311
+ it("should return correct value for && operator", () => {
312
+ expect(ChainedCommandExecutor.shouldExecute(true, "&&")).toBe(true);
313
+ expect(ChainedCommandExecutor.shouldExecute(false, "&&")).toBe(false);
314
+ });
315
+
316
+ it("should return correct value for || operator", () => {
317
+ expect(ChainedCommandExecutor.shouldExecute(true, "||")).toBe(false);
318
+ expect(ChainedCommandExecutor.shouldExecute(false, "||")).toBe(true);
319
+ });
320
+
321
+ it("should return true for ; operator", () => {
322
+ expect(ChainedCommandExecutor.shouldExecute(true, ";")).toBe(true);
323
+ expect(ChainedCommandExecutor.shouldExecute(false, ";")).toBe(true);
324
+ });
325
+ });
326
+ });
327
+
328
+ // =============================================================================
329
+ // T054: Pipe Parser Tests
330
+ // =============================================================================
331
+
332
+ describe("PipeParser", () => {
333
+ describe("parse", () => {
334
+ it("should parse single command without operators", () => {
335
+ const result = PipeParser.parse("/list");
336
+
337
+ expect(result.isPiped).toBe(false);
338
+ expect(result.segments).toHaveLength(1);
339
+ expect(result.segments[0]).toMatchObject({
340
+ type: "command",
341
+ value: "/list",
342
+ });
343
+ expect(result.hasRedirect).toBe(false);
344
+ });
345
+
346
+ it("should parse empty input", () => {
347
+ const result = PipeParser.parse("");
348
+
349
+ expect(result.isPiped).toBe(false);
350
+ expect(result.segments).toHaveLength(0);
351
+ });
352
+
353
+ it("should parse pipe operator (|)", () => {
354
+ const result = PipeParser.parse("/list | /filter");
355
+
356
+ expect(result.isPiped).toBe(true);
357
+ expect(result.segments).toHaveLength(2);
358
+ expect(result.segments[0]).toMatchObject({
359
+ type: "command",
360
+ value: "/list",
361
+ operator: "|",
362
+ });
363
+ expect(result.segments[1]).toMatchObject({
364
+ type: "command",
365
+ value: "/filter",
366
+ });
367
+ expect(result.hasRedirect).toBe(false);
368
+ });
369
+
370
+ it("should parse write redirect (>)", () => {
371
+ const result = PipeParser.parse("/list > output.txt");
372
+
373
+ expect(result.isPiped).toBe(true);
374
+ expect(result.segments).toHaveLength(2);
375
+ expect(result.segments[0]).toMatchObject({
376
+ type: "command",
377
+ value: "/list",
378
+ operator: ">",
379
+ });
380
+ expect(result.segments[1]).toMatchObject({
381
+ type: "file",
382
+ value: "output.txt",
383
+ });
384
+ expect(result.hasRedirect).toBe(true);
385
+ expect(result.redirectMode).toBe("overwrite");
386
+ expect(result.redirectTarget).toBe("output.txt");
387
+ });
388
+
389
+ it("should parse append redirect (>>)", () => {
390
+ const result = PipeParser.parse("/log >> history.txt");
391
+
392
+ expect(result.isPiped).toBe(true);
393
+ expect(result.hasRedirect).toBe(true);
394
+ expect(result.redirectMode).toBe("append");
395
+ expect(result.redirectTarget).toBe("history.txt");
396
+ });
397
+
398
+ it("should parse pipe with redirect", () => {
399
+ const result = PipeParser.parse("/list | /filter pattern > output.txt");
400
+
401
+ expect(result.isPiped).toBe(true);
402
+ expect(result.segments).toHaveLength(3);
403
+ expect(result.segments[0]).toMatchObject({
404
+ type: "command",
405
+ value: "/list",
406
+ operator: "|",
407
+ });
408
+ expect(result.segments[1]).toMatchObject({
409
+ type: "command",
410
+ value: "/filter pattern",
411
+ operator: ">",
412
+ });
413
+ expect(result.segments[2]).toMatchObject({
414
+ type: "file",
415
+ value: "output.txt",
416
+ });
417
+ expect(result.hasRedirect).toBe(true);
418
+ });
419
+
420
+ it("should NOT split on | inside quotes", () => {
421
+ const result = PipeParser.parse('/echo "foo | bar"');
422
+
423
+ expect(result.isPiped).toBe(false);
424
+ expect(result.segments).toHaveLength(1);
425
+ expect(result.segments[0]?.value).toBe('/echo "foo | bar"');
426
+ });
427
+
428
+ it("should NOT split on > inside quotes", () => {
429
+ const result = PipeParser.parse("/echo 'a > b'");
430
+
431
+ expect(result.isPiped).toBe(false);
432
+ expect(result.segments).toHaveLength(1);
433
+ });
434
+
435
+ it("should NOT confuse || (chain) with | (pipe)", () => {
436
+ const result = PipeParser.parse("/cmd || /other");
437
+
438
+ // || is a chain operator, not a pipe operator
439
+ expect(result.isPiped).toBe(false);
440
+ expect(result.hasRedirect).toBe(false);
441
+ });
442
+
443
+ it("should preserve command arguments", () => {
444
+ const result = PipeParser.parse("/list --all | /filter --pattern test");
445
+
446
+ expect(result.segments[0]?.value).toBe("/list --all");
447
+ expect(result.segments[1]?.value).toBe("/filter --pattern test");
448
+ });
449
+ });
450
+
451
+ describe("hasPipeOperators", () => {
452
+ it("should return true for |", () => {
453
+ expect(PipeParser.hasPipeOperators("/a | /b")).toBe(true);
454
+ });
455
+
456
+ it("should return true for >", () => {
457
+ expect(PipeParser.hasPipeOperators("/a > file")).toBe(true);
458
+ });
459
+
460
+ it("should return true for >>", () => {
461
+ expect(PipeParser.hasPipeOperators("/a >> file")).toBe(true);
462
+ });
463
+
464
+ it("should return false for single command", () => {
465
+ expect(PipeParser.hasPipeOperators("/help")).toBe(false);
466
+ });
467
+
468
+ it("should return false for || (chain operator)", () => {
469
+ expect(PipeParser.hasPipeOperators("/a || /b")).toBe(false);
470
+ });
471
+ });
472
+
473
+ describe("hasRedirection", () => {
474
+ it("should return true for >", () => {
475
+ expect(PipeParser.hasRedirection("/cmd > file")).toBe(true);
476
+ });
477
+
478
+ it("should return true for >>", () => {
479
+ expect(PipeParser.hasRedirection("/cmd >> file")).toBe(true);
480
+ });
481
+
482
+ it("should return false for |", () => {
483
+ expect(PipeParser.hasRedirection("/cmd1 | /cmd2")).toBe(false);
484
+ });
485
+
486
+ it("should return false for no operators", () => {
487
+ expect(PipeParser.hasRedirection("/cmd")).toBe(false);
488
+ });
489
+ });
490
+ });
491
+
492
+ describe("PipedCommandExecutor", () => {
493
+ describe("execute", () => {
494
+ it("should execute single command", async () => {
495
+ const executeFn = vi.fn().mockResolvedValue({
496
+ result: { kind: "success", message: "OK" },
497
+ output: "Hello",
498
+ });
499
+
500
+ const executor = new PipedCommandExecutor(executeFn);
501
+ const result = await executor.execute("/echo Hello");
502
+
503
+ expect(executeFn).toHaveBeenCalledTimes(1);
504
+ expect(executeFn).toHaveBeenCalledWith("/echo Hello", undefined, undefined);
505
+ expect(result.output).toBe("Hello");
506
+ expect(result.executedCount).toBe(1);
507
+ });
508
+
509
+ it("should pass output through pipe", async () => {
510
+ const executeFn = vi
511
+ .fn()
512
+ .mockResolvedValueOnce({
513
+ result: { kind: "success", message: "OK" },
514
+ output: "line1\nline2\nline3",
515
+ })
516
+ .mockResolvedValueOnce({
517
+ result: { kind: "success", message: "OK" },
518
+ output: "line2",
519
+ });
520
+
521
+ const executor = new PipedCommandExecutor(executeFn);
522
+ const result = await executor.execute("/list | /filter line2");
523
+
524
+ expect(executeFn).toHaveBeenCalledTimes(2);
525
+ // Second call should receive first output as input
526
+ expect(executeFn).toHaveBeenNthCalledWith(
527
+ 2,
528
+ "/filter line2",
529
+ "line1\nline2\nline3",
530
+ undefined
531
+ );
532
+ expect(result.output).toBe("line2");
533
+ });
534
+
535
+ it("should write output to file with >", async () => {
536
+ const executeFn = vi.fn().mockResolvedValue({
537
+ result: { kind: "success", message: "OK" },
538
+ output: "File content",
539
+ });
540
+
541
+ const writeFileFn = vi.fn().mockResolvedValue(undefined);
542
+
543
+ const executor = new PipedCommandExecutor(executeFn, writeFileFn);
544
+ const result = await executor.execute("/echo content > output.txt");
545
+
546
+ expect(writeFileFn).toHaveBeenCalledWith("output.txt", "File content", "overwrite");
547
+ expect(result.writtenFile).toBe("output.txt");
548
+ expect(result.completed).toBe(true);
549
+ });
550
+
551
+ it("should append output to file with >>", async () => {
552
+ const executeFn = vi.fn().mockResolvedValue({
553
+ result: { kind: "success", message: "OK" },
554
+ output: "New line",
555
+ });
556
+
557
+ const writeFileFn = vi.fn().mockResolvedValue(undefined);
558
+
559
+ const executor = new PipedCommandExecutor(executeFn, writeFileFn);
560
+ const result = await executor.execute("/log >> history.txt");
561
+
562
+ expect(writeFileFn).toHaveBeenCalledWith("history.txt", "New line", "append");
563
+ expect(result.writtenFile).toBe("history.txt");
564
+ });
565
+
566
+ it("should fail if no file writer configured for redirect", async () => {
567
+ const executeFn = vi.fn().mockResolvedValue({
568
+ result: { kind: "success", message: "OK" },
569
+ output: "content",
570
+ });
571
+
572
+ const executor = new PipedCommandExecutor(executeFn); // No writeFileFn
573
+ const result = await executor.execute("/cmd > file.txt");
574
+
575
+ expect(result.result.kind).toBe("error");
576
+ expect(result.completed).toBe(false);
577
+ });
578
+
579
+ it("should handle file write errors", async () => {
580
+ const executeFn = vi.fn().mockResolvedValue({
581
+ result: { kind: "success", message: "OK" },
582
+ output: "content",
583
+ });
584
+
585
+ const writeFileFn = vi.fn().mockRejectedValue(new Error("Permission denied"));
586
+
587
+ const executor = new PipedCommandExecutor(executeFn, writeFileFn);
588
+ const result = await executor.execute("/cmd > protected.txt");
589
+
590
+ expect(result.result.kind).toBe("error");
591
+ expect(result.completed).toBe(false);
592
+ });
593
+
594
+ it("should stop pipe on command error", async () => {
595
+ const executeFn = vi
596
+ .fn()
597
+ .mockResolvedValueOnce({
598
+ result: { kind: "error", code: "INTERNAL_ERROR", message: "Failed" },
599
+ output: "",
600
+ })
601
+ .mockResolvedValueOnce({
602
+ result: { kind: "success", message: "OK" },
603
+ output: "Should not run",
604
+ });
605
+
606
+ const executor = new PipedCommandExecutor(executeFn);
607
+ const result = await executor.execute("/cmd1 | /cmd2");
608
+
609
+ expect(executeFn).toHaveBeenCalledTimes(1);
610
+ expect(result.result.kind).toBe("error");
611
+ expect(result.completed).toBe(false);
612
+ });
613
+
614
+ it("should handle abort signal", async () => {
615
+ const controller = new AbortController();
616
+ controller.abort();
617
+
618
+ const executeFn = vi.fn();
619
+ const executor = new PipedCommandExecutor(executeFn);
620
+ const result = await executor.execute("/cmd1 | /cmd2", controller.signal);
621
+
622
+ expect(executeFn).not.toHaveBeenCalled();
623
+ expect(result.result.kind).toBe("error");
624
+ expect(result.completed).toBe(false);
625
+ });
626
+
627
+ it("should execute complex pipe chain correctly", async () => {
628
+ const executeFn = vi
629
+ .fn()
630
+ .mockResolvedValueOnce({
631
+ result: { kind: "success", message: "OK" },
632
+ output: "a\nb\nc\nd",
633
+ })
634
+ .mockResolvedValueOnce({
635
+ result: { kind: "success", message: "OK" },
636
+ output: "b\nc",
637
+ })
638
+ .mockResolvedValueOnce({
639
+ result: { kind: "success", message: "OK" },
640
+ output: "2 lines",
641
+ });
642
+
643
+ const writeFileFn = vi.fn().mockResolvedValue(undefined);
644
+
645
+ const executor = new PipedCommandExecutor(executeFn, writeFileFn);
646
+ const result = await executor.execute("/list | /filter | /count > result.txt");
647
+
648
+ expect(executeFn).toHaveBeenCalledTimes(3);
649
+ expect(writeFileFn).toHaveBeenCalledWith("result.txt", "2 lines", "overwrite");
650
+ expect(result.executedCount).toBe(3);
651
+ expect(result.completed).toBe(true);
652
+ });
653
+ });
654
+ });