@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,713 @@
1
+ /**
2
+ * E2E Tests for CLI Credential Commands
3
+ *
4
+ * Tests CLI commands and slash commands for credential management:
5
+ * - `vellum credentials list` - Show all stored credentials
6
+ * - `vellum credentials add <provider>` - Add credential interactively
7
+ * - `vellum credentials remove <provider>` - Remove credential
8
+ * - `/credentials` - Show credential status
9
+ * - `/auth` - Unified authentication command (set, clear, status)
10
+ *
11
+ * @module cli/__tests__/credentials
12
+ */
13
+
14
+ import type {
15
+ Credential,
16
+ CredentialRef,
17
+ CredentialSource,
18
+ CredentialStore,
19
+ CredentialStoreError,
20
+ } from "@vellum/core";
21
+ import { Err, Ok, type Result } from "@vellum/core";
22
+ import { describe, expect, it } from "vitest";
23
+
24
+ import {
25
+ authSlashCommands,
26
+ executeSlashCommand,
27
+ findSlashCommand,
28
+ getSlashCommandHelp,
29
+ isSlashCommand,
30
+ parseSlashCommand,
31
+ type SlashCommandContext,
32
+ } from "../commands/auth.js";
33
+
34
+ // =============================================================================
35
+ // Mock Credential Store
36
+ // =============================================================================
37
+
38
+ /**
39
+ * In-memory mock credential store for testing
40
+ */
41
+ class MockCredentialStore implements CredentialStore {
42
+ readonly name: CredentialSource;
43
+ readonly priority: number;
44
+ readonly readOnly: boolean;
45
+ private credentials: Map<string, Credential> = new Map();
46
+ private available: boolean = true;
47
+
48
+ constructor(options: {
49
+ name: CredentialSource;
50
+ priority?: number;
51
+ readOnly?: boolean;
52
+ available?: boolean;
53
+ initialCredentials?: Credential[];
54
+ }) {
55
+ this.name = options.name;
56
+ this.priority = options.priority ?? 50;
57
+ this.readOnly = options.readOnly ?? false;
58
+ this.available = options.available ?? true;
59
+
60
+ if (options.initialCredentials) {
61
+ for (const cred of options.initialCredentials) {
62
+ this.credentials.set(this.makeKey(cred.provider), cred);
63
+ }
64
+ }
65
+ }
66
+
67
+ private makeKey(provider: string, key?: string): string {
68
+ return key ? `${provider}:${key}` : provider;
69
+ }
70
+
71
+ setAvailable(available: boolean): void {
72
+ this.available = available;
73
+ }
74
+
75
+ async isAvailable(): Promise<Result<boolean, CredentialStoreError>> {
76
+ return Ok(this.available);
77
+ }
78
+
79
+ async get(
80
+ provider: string,
81
+ key?: string
82
+ ): Promise<Result<Credential | null, CredentialStoreError>> {
83
+ const credential = this.credentials.get(this.makeKey(provider, key));
84
+ return Ok(credential ?? null);
85
+ }
86
+
87
+ async set(credential: Credential): Promise<Result<void, CredentialStoreError>> {
88
+ if (this.readOnly) {
89
+ return Err({
90
+ code: "READ_ONLY",
91
+ message: "Store is read-only",
92
+ store: this.name,
93
+ });
94
+ }
95
+ this.credentials.set(this.makeKey(credential.provider), credential);
96
+ return Ok(undefined);
97
+ }
98
+
99
+ async delete(provider: string, key?: string): Promise<Result<boolean, CredentialStoreError>> {
100
+ if (this.readOnly) {
101
+ return Err({
102
+ code: "READ_ONLY",
103
+ message: "Store is read-only",
104
+ store: this.name,
105
+ });
106
+ }
107
+ const deleted = this.credentials.delete(this.makeKey(provider, key));
108
+ return Ok(deleted);
109
+ }
110
+
111
+ async exists(provider: string, key?: string): Promise<Result<boolean, CredentialStoreError>> {
112
+ return Ok(this.credentials.has(this.makeKey(provider, key)));
113
+ }
114
+
115
+ async list(provider?: string): Promise<Result<readonly CredentialRef[], CredentialStoreError>> {
116
+ const refs: CredentialRef[] = [];
117
+ for (const cred of this.credentials.values()) {
118
+ if (!provider || cred.provider === provider) {
119
+ refs.push({
120
+ id: cred.id,
121
+ provider: cred.provider,
122
+ type: cred.type,
123
+ source: cred.source,
124
+ maskedHint: `${cred.value.substring(0, 4)}****`,
125
+ metadata: cred.metadata,
126
+ createdAt: cred.createdAt,
127
+ });
128
+ }
129
+ }
130
+ return Ok(refs);
131
+ }
132
+
133
+ // Test helpers
134
+ getAll(): Credential[] {
135
+ return Array.from(this.credentials.values());
136
+ }
137
+
138
+ clear(): void {
139
+ this.credentials.clear();
140
+ }
141
+ }
142
+
143
+ // =============================================================================
144
+ // Mock Credential Manager
145
+ // =============================================================================
146
+
147
+ /**
148
+ * Mock CredentialManager for testing CLI commands
149
+ */
150
+ class MockCredentialManager {
151
+ private stores: MockCredentialStore[];
152
+ private preferredWriteStore: CredentialSource;
153
+
154
+ constructor(options: {
155
+ stores: MockCredentialStore[];
156
+ preferredWriteStore?: CredentialSource;
157
+ }) {
158
+ this.stores = options.stores;
159
+ this.preferredWriteStore = options.preferredWriteStore ?? "keychain";
160
+ }
161
+
162
+ async resolve(
163
+ provider: string,
164
+ key?: string
165
+ ): Promise<Result<Credential | null, CredentialStoreError>> {
166
+ for (const store of this.stores) {
167
+ const availResult = await store.isAvailable();
168
+ if (!availResult.ok || !availResult.value) continue;
169
+
170
+ const result = await store.get(provider, key);
171
+ if (result.ok && result.value) {
172
+ return result;
173
+ }
174
+ }
175
+ return Ok(null);
176
+ }
177
+
178
+ async store(input: {
179
+ provider: string;
180
+ type: string;
181
+ value: string;
182
+ metadata?: { label?: string };
183
+ }): Promise<Result<{ source: CredentialSource }, CredentialStoreError>> {
184
+ // Find preferred store if available
185
+ let store: MockCredentialStore | undefined;
186
+
187
+ // Try preferred store first
188
+ const preferredStore = this.stores.find(
189
+ (s) => s.name === this.preferredWriteStore && !s.readOnly
190
+ );
191
+ if (preferredStore) {
192
+ const availResult = await preferredStore.isAvailable();
193
+ if (availResult.ok && availResult.value) {
194
+ store = preferredStore;
195
+ }
196
+ }
197
+
198
+ // Fall back to any available writable store
199
+ if (!store) {
200
+ for (const s of this.stores) {
201
+ if (s.readOnly) continue;
202
+ const availResult = await s.isAvailable();
203
+ if (availResult.ok && availResult.value) {
204
+ store = s;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+
210
+ if (!store) {
211
+ return Err({
212
+ code: "STORE_UNAVAILABLE",
213
+ message: "No writable store available",
214
+ store: "runtime",
215
+ });
216
+ }
217
+
218
+ const credential: Credential = {
219
+ id: `${store.name}:${input.provider}`,
220
+ provider: input.provider,
221
+ type: input.type as
222
+ | "api_key"
223
+ | "oauth_token"
224
+ | "bearer_token"
225
+ | "service_account"
226
+ | "certificate",
227
+ value: input.value,
228
+ source: store.name,
229
+ metadata: input.metadata ?? {},
230
+ createdAt: new Date(),
231
+ };
232
+
233
+ const result = await store.set(credential);
234
+ if (!result.ok) return result as Result<never, CredentialStoreError>;
235
+
236
+ return Ok({ source: store.name });
237
+ }
238
+
239
+ async delete(provider: string, key?: string): Promise<Result<number, CredentialStoreError>> {
240
+ let count = 0;
241
+ for (const store of this.stores) {
242
+ if (store.readOnly) continue;
243
+ const result = await store.delete(provider, key);
244
+ if (result.ok && result.value) count++;
245
+ }
246
+ return Ok(count);
247
+ }
248
+
249
+ async exists(provider: string): Promise<Result<boolean, CredentialStoreError>> {
250
+ const result = await this.resolve(provider);
251
+ return Ok(result.ok && result.value !== null);
252
+ }
253
+
254
+ async list(provider?: string): Promise<Result<readonly CredentialRef[], CredentialStoreError>> {
255
+ const allRefs: CredentialRef[] = [];
256
+ for (const store of this.stores) {
257
+ const availResult = await store.isAvailable();
258
+ if (!availResult.ok || !availResult.value) continue;
259
+
260
+ const result = await store.list(provider);
261
+ if (result.ok) {
262
+ allRefs.push(...result.value);
263
+ }
264
+ }
265
+ return Ok(allRefs);
266
+ }
267
+
268
+ async getStoreAvailability(): Promise<Record<string, boolean>> {
269
+ const availability: Record<string, boolean> = {};
270
+ for (const store of this.stores) {
271
+ const result = await store.isAvailable();
272
+ availability[store.name] = result.ok && result.value;
273
+ }
274
+ return availability;
275
+ }
276
+ }
277
+
278
+ // =============================================================================
279
+ // Test Fixtures
280
+ // =============================================================================
281
+
282
+ function createTestCredential(provider: string, source: CredentialSource = "keychain"): Credential {
283
+ return {
284
+ id: `${source}:${provider}`,
285
+ provider,
286
+ type: "api_key",
287
+ value: `sk-${provider}-test-key-12345`,
288
+ source,
289
+ metadata: { label: `${provider} API Key` },
290
+ createdAt: new Date(),
291
+ };
292
+ }
293
+
294
+ function createMockStores(options?: {
295
+ keychainAvailable?: boolean;
296
+ envCredentials?: Credential[];
297
+ keychainCredentials?: Credential[];
298
+ fileCredentials?: Credential[];
299
+ }): MockCredentialStore[] {
300
+ return [
301
+ new MockCredentialStore({
302
+ name: "env",
303
+ priority: 100,
304
+ readOnly: true,
305
+ initialCredentials: options?.envCredentials,
306
+ }),
307
+ new MockCredentialStore({
308
+ name: "keychain",
309
+ priority: 75,
310
+ readOnly: false,
311
+ available: options?.keychainAvailable ?? true,
312
+ initialCredentials: options?.keychainCredentials,
313
+ }),
314
+ new MockCredentialStore({
315
+ name: "file",
316
+ priority: 50,
317
+ readOnly: false,
318
+ initialCredentials: options?.fileCredentials,
319
+ }),
320
+ ];
321
+ }
322
+
323
+ function createMockContext(options?: {
324
+ currentProvider?: string;
325
+ stores?: MockCredentialStore[];
326
+ keychainAvailable?: boolean;
327
+ envCredentials?: Credential[];
328
+ keychainCredentials?: Credential[];
329
+ fileCredentials?: Credential[];
330
+ }): SlashCommandContext {
331
+ const stores =
332
+ options?.stores ??
333
+ createMockStores({
334
+ keychainAvailable: options?.keychainAvailable,
335
+ envCredentials: options?.envCredentials,
336
+ keychainCredentials: options?.keychainCredentials,
337
+ fileCredentials: options?.fileCredentials,
338
+ });
339
+
340
+ return {
341
+ currentProvider: options?.currentProvider,
342
+ credentialManager: new MockCredentialManager({
343
+ stores,
344
+ preferredWriteStore: "keychain",
345
+ }) as unknown as SlashCommandContext["credentialManager"],
346
+ };
347
+ }
348
+
349
+ // =============================================================================
350
+ // Slash Command Parsing Tests
351
+ // =============================================================================
352
+
353
+ describe("Slash Command Parsing", () => {
354
+ describe("isSlashCommand", () => {
355
+ it("returns true for input starting with /", () => {
356
+ expect(isSlashCommand("/auth")).toBe(true);
357
+ expect(isSlashCommand("/auth set anthropic")).toBe(true);
358
+ expect(isSlashCommand("/credentials")).toBe(true);
359
+ });
360
+
361
+ it("returns true for input with leading whitespace", () => {
362
+ expect(isSlashCommand(" /auth")).toBe(true);
363
+ });
364
+
365
+ it("returns false for non-slash input", () => {
366
+ expect(isSlashCommand("login")).toBe(false);
367
+ expect(isSlashCommand("help")).toBe(false);
368
+ expect(isSlashCommand("")).toBe(false);
369
+ });
370
+ });
371
+
372
+ describe("parseSlashCommand", () => {
373
+ it("parses command without arguments", () => {
374
+ const result = parseSlashCommand("/credentials");
375
+ expect(result).toEqual({ command: "credentials", args: [] });
376
+ });
377
+
378
+ it("parses command with single argument", () => {
379
+ const result = parseSlashCommand("/credentials anthropic");
380
+ expect(result).toEqual({ command: "credentials", args: ["anthropic"] });
381
+ });
382
+
383
+ it("parses command with multiple arguments", () => {
384
+ const result = parseSlashCommand("/help credentials details");
385
+ expect(result).toEqual({ command: "help", args: ["credentials", "details"] });
386
+ });
387
+
388
+ it("normalizes command to lowercase", () => {
389
+ const result = parseSlashCommand("/CREDENTIALS");
390
+ expect(result).toEqual({ command: "credentials", args: [] });
391
+ });
392
+
393
+ it("handles multiple spaces between arguments", () => {
394
+ const result = parseSlashCommand("/credentials anthropic extra");
395
+ expect(result).toEqual({ command: "credentials", args: ["anthropic", "extra"] });
396
+ });
397
+
398
+ it("returns null for non-slash input", () => {
399
+ expect(parseSlashCommand("credentials")).toBeNull();
400
+ expect(parseSlashCommand("")).toBeNull();
401
+ });
402
+ });
403
+
404
+ describe("findSlashCommand", () => {
405
+ it("finds command by name", () => {
406
+ const cmd = findSlashCommand("credentials");
407
+ expect(cmd).toBeDefined();
408
+ expect(cmd?.name).toBe("credentials");
409
+ });
410
+
411
+ it("finds command by alias", () => {
412
+ const cmd = findSlashCommand("creds");
413
+ expect(cmd).toBeDefined();
414
+ expect(cmd?.name).toBe("credentials");
415
+ });
416
+
417
+ it("returns undefined for unknown command", () => {
418
+ const cmd = findSlashCommand("unknown");
419
+ expect(cmd).toBeUndefined();
420
+ });
421
+
422
+ it("is case-insensitive", () => {
423
+ const cmd = findSlashCommand("CREDENTIALS");
424
+ expect(cmd).toBeDefined();
425
+ expect(cmd?.name).toBe("credentials");
426
+ });
427
+ });
428
+ });
429
+
430
+ // =============================================================================
431
+ // /credentials Command Tests
432
+ // =============================================================================
433
+
434
+ describe("/credentials Command", () => {
435
+ describe("Output Format", () => {
436
+ it("shows header and dividers", async () => {
437
+ const context = createMockContext();
438
+ const result = await executeSlashCommand("/credentials", context);
439
+
440
+ expect(result.success).toBe(true);
441
+ expect(result.message).toContain("🔐 Credential Status");
442
+ expect(result.message).toContain("━");
443
+ });
444
+
445
+ it("shows storage backend status", async () => {
446
+ const context = createMockContext({
447
+ keychainAvailable: true,
448
+ });
449
+ const result = await executeSlashCommand("/credentials", context);
450
+
451
+ expect(result.message).toContain("📦 Storage Backends:");
452
+ expect(result.message).toContain("✓ keychain");
453
+ expect(result.message).toContain("✓ file");
454
+ expect(result.message).toContain("✓ env");
455
+ });
456
+
457
+ it("shows unavailable backends with ✗", async () => {
458
+ const stores = createMockStores({ keychainAvailable: false });
459
+ stores.find((s) => s.name === "keychain")?.setAvailable(false);
460
+ const context = createMockContext({ stores });
461
+
462
+ const result = await executeSlashCommand("/credentials", context);
463
+
464
+ expect(result.message).toContain("✗ keychain");
465
+ });
466
+ });
467
+
468
+ describe("Credential Listing", () => {
469
+ it("shows message when no credentials", async () => {
470
+ const context = createMockContext();
471
+ const result = await executeSlashCommand("/credentials", context);
472
+
473
+ expect(result.message).toContain("No credentials stored");
474
+ // Note: The legacy slash command handler still references /login in output
475
+ // The enhanced authCommand uses /auth set. This is expected behavior.
476
+ });
477
+
478
+ it("lists stored credentials with masked values", async () => {
479
+ const context = createMockContext({
480
+ keychainCredentials: [
481
+ createTestCredential("anthropic", "keychain"),
482
+ createTestCredential("openai", "keychain"),
483
+ ],
484
+ });
485
+ const result = await executeSlashCommand("/credentials", context);
486
+
487
+ expect(result.message).toContain("🔑 Credentials:");
488
+ expect(result.message).toContain("anthropic (keychain)");
489
+ expect(result.message).toContain("openai (keychain)");
490
+ expect(result.message).toContain("sk-a****");
491
+ expect(result.message).toContain("[api_key]");
492
+ });
493
+
494
+ it("filters by provider when specified", async () => {
495
+ const context = createMockContext({
496
+ keychainCredentials: [createTestCredential("anthropic"), createTestCredential("openai")],
497
+ });
498
+ const result = await executeSlashCommand("/credentials anthropic", context);
499
+
500
+ expect(result.message).toContain("anthropic");
501
+ expect(result.message).not.toContain("openai");
502
+ });
503
+
504
+ it("shows message when filtered provider not found", async () => {
505
+ const context = createMockContext({
506
+ keychainCredentials: [createTestCredential("anthropic")],
507
+ });
508
+ const result = await executeSlashCommand("/credentials google", context);
509
+
510
+ expect(result.message).toContain("No credential found for google");
511
+ });
512
+ });
513
+
514
+ describe("Data Property", () => {
515
+ it("includes availability in data", async () => {
516
+ const context = createMockContext();
517
+ const result = await executeSlashCommand("/credentials", context);
518
+
519
+ expect(result.data?.availability).toBeDefined();
520
+ expect(result.data?.availability).toHaveProperty("keychain");
521
+ expect(result.data?.availability).toHaveProperty("file");
522
+ expect(result.data?.availability).toHaveProperty("env");
523
+ });
524
+
525
+ it("includes credentials array in data", async () => {
526
+ const context = createMockContext({
527
+ keychainCredentials: [createTestCredential("anthropic")],
528
+ });
529
+ const result = await executeSlashCommand("/credentials", context);
530
+
531
+ expect(result.data?.credentials).toBeDefined();
532
+ expect(Array.isArray(result.data?.credentials)).toBe(true);
533
+ const creds = result.data?.credentials as unknown[];
534
+ expect(creds).toHaveLength(1);
535
+ });
536
+ });
537
+
538
+ describe("Aliases", () => {
539
+ it("works with /creds alias", async () => {
540
+ const context = createMockContext();
541
+ const result = await executeSlashCommand("/creds", context);
542
+
543
+ expect(result.success).toBe(true);
544
+ expect(result.message).toContain("Credential Status");
545
+ });
546
+
547
+ it("works with /keys alias", async () => {
548
+ const context = createMockContext();
549
+ const result = await executeSlashCommand("/keys", context);
550
+
551
+ expect(result.success).toBe(true);
552
+ expect(result.message).toContain("Credential Status");
553
+ });
554
+ });
555
+ });
556
+
557
+ // =============================================================================
558
+ // Unknown Command Handling Tests
559
+ // =============================================================================
560
+
561
+ describe("Unknown Command Handling", () => {
562
+ it("shows error for unknown command", async () => {
563
+ const context = createMockContext();
564
+ const result = await executeSlashCommand("/unknown", context);
565
+
566
+ expect(result.success).toBe(false);
567
+ expect(result.message).toContain("Unknown command");
568
+ expect(result.message).toContain("/unknown");
569
+ });
570
+
571
+ it("lists available commands on unknown", async () => {
572
+ const context = createMockContext();
573
+ const result = await executeSlashCommand("/notreal", context);
574
+
575
+ expect(result.message).toContain("Available:");
576
+ expect(result.message).toContain("/credentials");
577
+ });
578
+
579
+ it("provides help for specific command", async () => {
580
+ const context = createMockContext();
581
+ const result = await executeSlashCommand("/help credentials", context);
582
+
583
+ expect(result.success).toBe(true);
584
+ expect(result.message).toContain("credentials");
585
+ expect(result.message).toContain("Usage:");
586
+ });
587
+ });
588
+
589
+ // =============================================================================
590
+ // Help System Tests
591
+ // =============================================================================
592
+
593
+ describe("getSlashCommandHelp", () => {
594
+ it("lists all available commands", () => {
595
+ const help = getSlashCommandHelp();
596
+
597
+ expect(help).toContain("Available Commands:");
598
+ expect(help).toContain("/credentials");
599
+ });
600
+
601
+ it("shows aliases for commands", () => {
602
+ const help = getSlashCommandHelp();
603
+
604
+ expect(help).toContain("Aliases:");
605
+ expect(help).toContain("/creds");
606
+ });
607
+
608
+ it("shows usage patterns", () => {
609
+ const help = getSlashCommandHelp();
610
+
611
+ expect(help).toContain("Usage:");
612
+ });
613
+ });
614
+
615
+ // =============================================================================
616
+ // Command Registry Tests
617
+ // =============================================================================
618
+
619
+ describe("authSlashCommands Registry", () => {
620
+ it("contains credentials command", () => {
621
+ const cmd = authSlashCommands.find((c) => c.name === "credentials");
622
+ expect(cmd).toBeDefined();
623
+ expect(cmd?.aliases).toContain("creds");
624
+ expect(cmd?.aliases).toContain("keys");
625
+ });
626
+
627
+ it("all commands have required properties", () => {
628
+ for (const cmd of authSlashCommands) {
629
+ expect(cmd.name).toBeDefined();
630
+ expect(cmd.description).toBeDefined();
631
+ expect(cmd.usage).toBeDefined();
632
+ expect(cmd.handler).toBeDefined();
633
+ expect(typeof cmd.handler).toBe("function");
634
+ }
635
+ });
636
+ });
637
+
638
+ // =============================================================================
639
+ // Edge Cases and Error Handling
640
+ // =============================================================================
641
+
642
+ describe("Edge Cases", () => {
643
+ describe("Provider Name Handling", () => {
644
+ it("normalizes provider names to lowercase in credentials display", async () => {
645
+ const stores = createMockStores({
646
+ keychainCredentials: [createTestCredential("anthropic")],
647
+ });
648
+ const context = createMockContext({ stores });
649
+
650
+ const result = await executeSlashCommand("/credentials", context);
651
+ expect(result.message).toContain("anthropic");
652
+ });
653
+ });
654
+
655
+ describe("Multiple Credentials", () => {
656
+ it("handles multiple credentials from same provider", async () => {
657
+ const stores = createMockStores({
658
+ keychainCredentials: [createTestCredential("anthropic")],
659
+ fileCredentials: [createTestCredential("anthropic", "file")],
660
+ });
661
+ const context = createMockContext({ stores });
662
+
663
+ const result = await executeSlashCommand("/credentials", context);
664
+ // Both should be listed
665
+ expect(result.data?.credentials).toBeDefined();
666
+ const creds = result.data?.credentials as { provider: string }[];
667
+ const anthropicCreds = creds.filter((c) => c.provider === "anthropic");
668
+ expect(anthropicCreds).toHaveLength(2);
669
+ });
670
+ });
671
+ });
672
+
673
+ // =============================================================================
674
+ // Integration Scenarios
675
+ // =============================================================================
676
+
677
+ describe("Integration Scenarios", () => {
678
+ it("displays credentials from multiple sources", async () => {
679
+ const stores = createMockStores({
680
+ keychainCredentials: [createTestCredential("anthropic")],
681
+ fileCredentials: [createTestCredential("openai", "file")],
682
+ envCredentials: [createTestCredential("google", "env")],
683
+ });
684
+ const context = createMockContext({ stores });
685
+
686
+ // Verify all are listed
687
+ const credsResult = await executeSlashCommand("/credentials", context);
688
+ expect(credsResult.message).toContain("anthropic");
689
+ expect(credsResult.message).toContain("openai");
690
+ expect(credsResult.message).toContain("google");
691
+ });
692
+
693
+ it("filters credentials by provider", async () => {
694
+ const stores = createMockStores({
695
+ keychainCredentials: [createTestCredential("anthropic"), createTestCredential("openai")],
696
+ });
697
+ const context = createMockContext({ stores });
698
+
699
+ const result = await executeSlashCommand("/credentials anthropic", context);
700
+ expect(result.message).toContain("anthropic");
701
+ expect(result.message).not.toContain("openai");
702
+ });
703
+
704
+ it("shows store availability status", async () => {
705
+ const stores = createMockStores({ keychainAvailable: false });
706
+ stores.find((s) => s.name === "keychain")?.setAvailable(false);
707
+ const context = createMockContext({ stores });
708
+
709
+ const result = await executeSlashCommand("/credentials", context);
710
+ expect(result.message).toContain("✗ keychain");
711
+ expect(result.message).toContain("✓ file");
712
+ });
713
+ });