@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,597 @@
1
+ /**
2
+ * User Commands Tests
3
+ *
4
+ * Tests for user-defined command loading:
5
+ * - Directory scanning
6
+ * - Command validation
7
+ * - Registry integration
8
+ * - Error handling
9
+ *
10
+ * @module cli/commands/__tests__/user-commands
11
+ */
12
+
13
+ import * as fs from "node:fs/promises";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
+
19
+ import { CommandRegistry } from "../registry.js";
20
+ import type { CommandError, CommandResult, CommandSuccess } from "../types.js";
21
+ import {
22
+ ensureCommandsDirectory,
23
+ getCommandTemplate,
24
+ registerUserCommands,
25
+ UserCommandLoader,
26
+ } from "../user-commands.js";
27
+
28
+ // =============================================================================
29
+ // Test Setup
30
+ // =============================================================================
31
+
32
+ const TEST_DIR = path.join(os.tmpdir(), "vellum-user-commands-test");
33
+ const COMMANDS_DIR = path.join(TEST_DIR, "commands");
34
+
35
+ /**
36
+ * Type assertion helpers
37
+ */
38
+ function assertSuccess(result: CommandResult): asserts result is CommandSuccess {
39
+ expect(result.kind).toBe("success");
40
+ }
41
+
42
+ function assertError(result: CommandResult): asserts result is CommandError {
43
+ expect(result.kind).toBe("error");
44
+ }
45
+
46
+ /**
47
+ * Valid command file content (JavaScript)
48
+ */
49
+ const VALID_COMMAND_JS = `
50
+ export default {
51
+ name: '/test-cmd',
52
+ description: 'A test command',
53
+ execute: async (args, context) => {
54
+ return {
55
+ success: true,
56
+ message: 'Test executed: ' + args.raw,
57
+ data: { args, context },
58
+ };
59
+ },
60
+ };
61
+ `;
62
+
63
+ /**
64
+ * Valid command with aliases
65
+ */
66
+ const VALID_COMMAND_WITH_ALIASES = `
67
+ export default {
68
+ name: '/greet',
69
+ description: 'Greeting command',
70
+ aliases: ['g', 'hello'],
71
+ category: 'tools',
72
+ execute: async (args) => {
73
+ const name = args.positional[0] || 'World';
74
+ return {
75
+ success: true,
76
+ message: 'Hello, ' + name + '!',
77
+ };
78
+ },
79
+ };
80
+ `;
81
+
82
+ /**
83
+ * Command that returns error
84
+ */
85
+ const ERROR_COMMAND = `
86
+ export default {
87
+ name: '/fail',
88
+ description: 'A command that fails',
89
+ execute: async () => {
90
+ return {
91
+ success: false,
92
+ error: 'Intentional failure',
93
+ };
94
+ },
95
+ };
96
+ `;
97
+
98
+ /**
99
+ * Command that throws
100
+ */
101
+ const THROWING_COMMAND = `
102
+ export default {
103
+ name: '/throws',
104
+ description: 'A command that throws',
105
+ execute: async () => {
106
+ throw new Error('Unexpected error');
107
+ },
108
+ };
109
+ `;
110
+
111
+ /**
112
+ * Invalid: missing name
113
+ */
114
+ const INVALID_NO_NAME = `
115
+ export default {
116
+ description: 'Missing name',
117
+ execute: async () => ({ success: true }),
118
+ };
119
+ `;
120
+
121
+ /**
122
+ * Invalid: missing description
123
+ */
124
+ const INVALID_NO_DESCRIPTION = `
125
+ export default {
126
+ name: '/no-desc',
127
+ execute: async () => ({ success: true }),
128
+ };
129
+ `;
130
+
131
+ /**
132
+ * Invalid: missing execute
133
+ */
134
+ const INVALID_NO_EXECUTE = `
135
+ export default {
136
+ name: '/no-exec',
137
+ description: 'Missing execute',
138
+ };
139
+ `;
140
+
141
+ /**
142
+ * Invalid: name doesn't start with /
143
+ */
144
+ const INVALID_NAME_FORMAT = `
145
+ export default {
146
+ name: 'bad-name',
147
+ description: 'Name without slash',
148
+ execute: async () => ({ success: true }),
149
+ };
150
+ `;
151
+
152
+ /**
153
+ * Invalid: bad category
154
+ */
155
+ const INVALID_CATEGORY = `
156
+ export default {
157
+ name: '/bad-cat',
158
+ description: 'Invalid category',
159
+ category: 'invalid-category',
160
+ execute: async () => ({ success: true }),
161
+ };
162
+ `;
163
+
164
+ /**
165
+ * Invalid: no default export
166
+ */
167
+ const NO_DEFAULT_EXPORT = `
168
+ export const command = {
169
+ name: '/no-default',
170
+ description: 'No default export',
171
+ execute: async () => ({ success: true }),
172
+ };
173
+ `;
174
+
175
+ /**
176
+ * Write test command file
177
+ */
178
+ async function writeCommand(filename: string, content: string): Promise<string> {
179
+ const filePath = path.join(COMMANDS_DIR, filename);
180
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
181
+ await fs.writeFile(filePath, content, "utf-8");
182
+ return filePath;
183
+ }
184
+
185
+ /**
186
+ * Create mock command context
187
+ */
188
+ function createMockContext() {
189
+ return {
190
+ session: {
191
+ id: "test-session",
192
+ provider: "anthropic",
193
+ cwd: TEST_DIR,
194
+ },
195
+ credentials: {
196
+ resolve: vi.fn(),
197
+ store: vi.fn(),
198
+ delete: vi.fn(),
199
+ list: vi.fn(),
200
+ },
201
+ toolRegistry: {
202
+ get: vi.fn(),
203
+ list: vi.fn(),
204
+ },
205
+ parsedArgs: {
206
+ command: "test",
207
+ positional: [] as string[],
208
+ named: {} as Record<string, string | boolean>,
209
+ raw: "/test",
210
+ },
211
+ emit: vi.fn(),
212
+ };
213
+ }
214
+
215
+ // =============================================================================
216
+ // Setup/Teardown
217
+ // =============================================================================
218
+
219
+ beforeEach(async () => {
220
+ await fs.mkdir(COMMANDS_DIR, { recursive: true });
221
+ });
222
+
223
+ afterEach(async () => {
224
+ try {
225
+ await fs.rm(TEST_DIR, { recursive: true, force: true });
226
+ } catch {
227
+ // Ignore cleanup errors
228
+ }
229
+ vi.restoreAllMocks();
230
+ });
231
+
232
+ // =============================================================================
233
+ // UserCommandLoader Tests
234
+ // =============================================================================
235
+
236
+ describe("UserCommandLoader", () => {
237
+ describe("constructor", () => {
238
+ it("should use default ~/.vellum path when no baseDir provided", () => {
239
+ const loader = new UserCommandLoader();
240
+ const expectedDir = path.join(os.homedir(), ".vellum", "commands");
241
+ expect(loader.getCommandsDir()).toBe(expectedDir);
242
+ });
243
+
244
+ it("should use custom baseDir when provided", () => {
245
+ const loader = new UserCommandLoader(TEST_DIR);
246
+ expect(loader.getCommandsDir()).toBe(COMMANDS_DIR);
247
+ });
248
+ });
249
+
250
+ describe("directoryExists", () => {
251
+ it("should return true when directory exists", async () => {
252
+ const loader = new UserCommandLoader(TEST_DIR);
253
+ const exists = await loader.directoryExists();
254
+ expect(exists).toBe(true);
255
+ });
256
+
257
+ it("should return false when directory does not exist", async () => {
258
+ const loader = new UserCommandLoader(path.join(TEST_DIR, "nonexistent"));
259
+ const exists = await loader.directoryExists();
260
+ expect(exists).toBe(false);
261
+ });
262
+ });
263
+
264
+ describe("load", () => {
265
+ it("should return empty result when directory does not exist", async () => {
266
+ const loader = new UserCommandLoader(path.join(TEST_DIR, "nonexistent"));
267
+ const result = await loader.load();
268
+
269
+ expect(result.commands).toHaveLength(0);
270
+ expect(result.errors).toHaveLength(0);
271
+ expect(result.scanned).toBe(0);
272
+ });
273
+
274
+ it("should return empty result when directory is empty", async () => {
275
+ const loader = new UserCommandLoader(TEST_DIR);
276
+ const result = await loader.load();
277
+
278
+ expect(result.commands).toHaveLength(0);
279
+ expect(result.errors).toHaveLength(0);
280
+ expect(result.scanned).toBe(0);
281
+ });
282
+
283
+ it("should load valid .js command file", async () => {
284
+ await writeCommand("test-cmd.js", VALID_COMMAND_JS);
285
+
286
+ const loader = new UserCommandLoader(TEST_DIR);
287
+ const result = await loader.load();
288
+
289
+ expect(result.commands).toHaveLength(1);
290
+ expect(result.errors).toHaveLength(0);
291
+ expect(result.scanned).toBe(1);
292
+
293
+ const cmd = result.commands[0];
294
+ expect(cmd).toBeDefined();
295
+ expect(cmd?.name).toBe("test-cmd");
296
+ expect(cmd?.description).toBe("A test command");
297
+ expect(cmd?.kind).toBe("user");
298
+ expect(cmd?.category).toBe("tools");
299
+ });
300
+
301
+ it("should load valid .mjs command file", async () => {
302
+ await writeCommand("test-cmd.mjs", VALID_COMMAND_JS);
303
+
304
+ const loader = new UserCommandLoader(TEST_DIR);
305
+ const result = await loader.load();
306
+
307
+ expect(result.commands).toHaveLength(1);
308
+ expect(result.errors).toHaveLength(0);
309
+ });
310
+
311
+ it("should load command with aliases", async () => {
312
+ await writeCommand("greet.js", VALID_COMMAND_WITH_ALIASES);
313
+
314
+ const loader = new UserCommandLoader(TEST_DIR);
315
+ const result = await loader.load();
316
+
317
+ expect(result.commands).toHaveLength(1);
318
+ const cmd = result.commands[0];
319
+ expect(cmd).toBeDefined();
320
+ expect(cmd?.name).toBe("greet");
321
+ expect(cmd?.aliases).toEqual(["g", "hello"]);
322
+ expect(cmd?.category).toBe("tools");
323
+ });
324
+
325
+ it("should ignore non-command files", async () => {
326
+ await writeCommand("test-cmd.js", VALID_COMMAND_JS);
327
+ await fs.writeFile(path.join(COMMANDS_DIR, "readme.txt"), "Not a command");
328
+ await fs.writeFile(path.join(COMMANDS_DIR, "data.json"), "{}");
329
+
330
+ const loader = new UserCommandLoader(TEST_DIR);
331
+ const result = await loader.load();
332
+
333
+ expect(result.commands).toHaveLength(1);
334
+ expect(result.scanned).toBe(1);
335
+ });
336
+
337
+ it("should load multiple command files", async () => {
338
+ await writeCommand("cmd1.js", VALID_COMMAND_JS);
339
+ await writeCommand("greet.mjs", VALID_COMMAND_WITH_ALIASES);
340
+
341
+ const loader = new UserCommandLoader(TEST_DIR);
342
+ const result = await loader.load();
343
+
344
+ expect(result.commands).toHaveLength(2);
345
+ expect(result.errors).toHaveLength(0);
346
+ expect(result.scanned).toBe(2);
347
+ });
348
+ });
349
+
350
+ describe("validation errors", () => {
351
+ it("should report error for missing name", async () => {
352
+ await writeCommand("invalid-no-name.js", INVALID_NO_NAME);
353
+
354
+ const loader = new UserCommandLoader(TEST_DIR);
355
+ const result = await loader.load();
356
+
357
+ expect(result.commands).toHaveLength(0);
358
+ expect(result.errors).toHaveLength(1);
359
+ expect(result.errors[0]?.error).toContain("name");
360
+ });
361
+
362
+ it("should report error for missing description", async () => {
363
+ await writeCommand("invalid-no-desc.js", INVALID_NO_DESCRIPTION);
364
+
365
+ const loader = new UserCommandLoader(TEST_DIR);
366
+ const result = await loader.load();
367
+
368
+ expect(result.commands).toHaveLength(0);
369
+ expect(result.errors).toHaveLength(1);
370
+ expect(result.errors[0]?.error).toContain("description");
371
+ });
372
+
373
+ it("should report error for missing execute", async () => {
374
+ await writeCommand("invalid-no-exec.js", INVALID_NO_EXECUTE);
375
+
376
+ const loader = new UserCommandLoader(TEST_DIR);
377
+ const result = await loader.load();
378
+
379
+ expect(result.commands).toHaveLength(0);
380
+ expect(result.errors).toHaveLength(1);
381
+ expect(result.errors[0]?.error).toContain("execute");
382
+ });
383
+
384
+ it("should report error for name not starting with /", async () => {
385
+ await writeCommand("invalid-name-format.js", INVALID_NAME_FORMAT);
386
+
387
+ const loader = new UserCommandLoader(TEST_DIR);
388
+ const result = await loader.load();
389
+
390
+ expect(result.commands).toHaveLength(0);
391
+ expect(result.errors).toHaveLength(1);
392
+ expect(result.errors[0]?.error).toContain("must start with");
393
+ });
394
+
395
+ it("should report error for invalid category", async () => {
396
+ await writeCommand("invalid-category.js", INVALID_CATEGORY);
397
+
398
+ const loader = new UserCommandLoader(TEST_DIR);
399
+ const result = await loader.load();
400
+
401
+ expect(result.commands).toHaveLength(0);
402
+ expect(result.errors).toHaveLength(1);
403
+ expect(result.errors[0]?.error).toContain("Invalid category");
404
+ });
405
+
406
+ it("should report error for no default export", async () => {
407
+ await writeCommand("invalid-no-default.js", NO_DEFAULT_EXPORT);
408
+
409
+ const loader = new UserCommandLoader(TEST_DIR);
410
+ const result = await loader.load();
411
+
412
+ expect(result.commands).toHaveLength(0);
413
+ expect(result.errors).toHaveLength(1);
414
+ expect(result.errors[0]?.error).toContain("No default export");
415
+ });
416
+
417
+ it("should report error for syntax errors in file", async () => {
418
+ await writeCommand("invalid.js", "export default { invalid syntax");
419
+
420
+ const loader = new UserCommandLoader(TEST_DIR);
421
+ const result = await loader.load();
422
+
423
+ expect(result.commands).toHaveLength(0);
424
+ expect(result.errors).toHaveLength(1);
425
+ // Error message will be from the parser
426
+ });
427
+ });
428
+
429
+ describe("command execution", () => {
430
+ it("should execute loaded command successfully", async () => {
431
+ await writeCommand("test-cmd.js", VALID_COMMAND_JS);
432
+
433
+ const loader = new UserCommandLoader(TEST_DIR);
434
+ const result = await loader.load();
435
+
436
+ expect(result.commands).toHaveLength(1);
437
+
438
+ const cmd = result.commands[0];
439
+ expect(cmd).toBeDefined();
440
+ if (!cmd) return;
441
+ const ctx = createMockContext();
442
+ ctx.parsedArgs.raw = "/test-cmd hello world";
443
+
444
+ const execResult = await cmd.execute(ctx as unknown as Parameters<typeof cmd.execute>[0]);
445
+ assertSuccess(execResult);
446
+ expect(execResult.message).toContain("Test executed");
447
+ expect(execResult.message).toContain("hello world");
448
+ });
449
+
450
+ it("should pass context to command", async () => {
451
+ await writeCommand("test-cmd.js", VALID_COMMAND_JS);
452
+
453
+ const loader = new UserCommandLoader(TEST_DIR);
454
+ const result = await loader.load();
455
+
456
+ const cmd = result.commands[0];
457
+ expect(cmd).toBeDefined();
458
+ if (!cmd) return;
459
+ const ctx = createMockContext();
460
+ ctx.session.cwd = "/my/cwd";
461
+ ctx.session.id = "my-session";
462
+ ctx.session.provider = "openai";
463
+
464
+ const execResult = await cmd.execute(ctx as unknown as Parameters<typeof cmd.execute>[0]);
465
+ assertSuccess(execResult);
466
+
467
+ const data = execResult.data as {
468
+ context: { cwd: string; sessionId: string; provider: string };
469
+ };
470
+ expect(data.context.cwd).toBe("/my/cwd");
471
+ expect(data.context.sessionId).toBe("my-session");
472
+ expect(data.context.provider).toBe("openai");
473
+ });
474
+
475
+ it("should handle command that returns error", async () => {
476
+ await writeCommand("fail.js", ERROR_COMMAND);
477
+
478
+ const loader = new UserCommandLoader(TEST_DIR);
479
+ const result = await loader.load();
480
+
481
+ const cmd = result.commands[0];
482
+ expect(cmd).toBeDefined();
483
+ if (!cmd) return;
484
+ const ctx = createMockContext();
485
+
486
+ const execResult = await cmd.execute(ctx as unknown as Parameters<typeof cmd.execute>[0]);
487
+ assertError(execResult);
488
+ expect(execResult.message).toBe("Intentional failure");
489
+ });
490
+
491
+ it("should handle command that throws", async () => {
492
+ await writeCommand("throws.js", THROWING_COMMAND);
493
+
494
+ const loader = new UserCommandLoader(TEST_DIR);
495
+ const result = await loader.load();
496
+
497
+ const cmd = result.commands[0];
498
+ expect(cmd).toBeDefined();
499
+ if (!cmd) return;
500
+ const ctx = createMockContext();
501
+
502
+ const execResult = await cmd.execute(ctx as unknown as Parameters<typeof cmd.execute>[0]);
503
+ assertError(execResult);
504
+ expect(execResult.message).toBe("Unexpected error");
505
+ });
506
+ });
507
+ });
508
+
509
+ // =============================================================================
510
+ // Registry Integration Tests
511
+ // =============================================================================
512
+
513
+ describe("registerUserCommands", () => {
514
+ it("should register commands in registry", async () => {
515
+ await writeCommand("test-cmd.js", VALID_COMMAND_JS);
516
+ await writeCommand("greet.js", VALID_COMMAND_WITH_ALIASES);
517
+
518
+ const registry = new CommandRegistry();
519
+ const result = await registerUserCommands(registry, { baseDir: TEST_DIR });
520
+
521
+ expect(result.commands).toHaveLength(2);
522
+ expect(registry.size).toBe(2);
523
+ expect(registry.has("test-cmd")).toBe(true);
524
+ expect(registry.has("greet")).toBe(true);
525
+ // Check alias
526
+ expect(registry.has("g")).toBe(true);
527
+ expect(registry.has("hello")).toBe(true);
528
+ });
529
+
530
+ it("should return errors for invalid commands", async () => {
531
+ await writeCommand("valid.js", VALID_COMMAND_JS);
532
+ await writeCommand("invalid.js", INVALID_NO_NAME);
533
+
534
+ const registry = new CommandRegistry();
535
+ const result = await registerUserCommands(registry, { baseDir: TEST_DIR });
536
+
537
+ expect(result.commands).toHaveLength(1);
538
+ expect(result.errors).toHaveLength(1);
539
+ expect(registry.size).toBe(1);
540
+ });
541
+
542
+ it("should work with empty directory", async () => {
543
+ const registry = new CommandRegistry();
544
+ const result = await registerUserCommands(registry, { baseDir: TEST_DIR });
545
+
546
+ expect(result.commands).toHaveLength(0);
547
+ expect(result.errors).toHaveLength(0);
548
+ expect(registry.size).toBe(0);
549
+ });
550
+
551
+ it("should work with non-existent directory", async () => {
552
+ const registry = new CommandRegistry();
553
+ const result = await registerUserCommands(registry, {
554
+ baseDir: path.join(TEST_DIR, "nonexistent"),
555
+ });
556
+
557
+ expect(result.commands).toHaveLength(0);
558
+ expect(result.errors).toHaveLength(0);
559
+ expect(registry.size).toBe(0);
560
+ });
561
+ });
562
+
563
+ // =============================================================================
564
+ // Utility Function Tests
565
+ // =============================================================================
566
+
567
+ describe("ensureCommandsDirectory", () => {
568
+ it("should create commands directory", async () => {
569
+ const newDir = path.join(TEST_DIR, "new-vellum");
570
+ const commandsDir = await ensureCommandsDirectory(newDir);
571
+
572
+ expect(commandsDir).toBe(path.join(newDir, "commands"));
573
+
574
+ const stat = await fs.stat(commandsDir);
575
+ expect(stat.isDirectory()).toBe(true);
576
+ });
577
+
578
+ it("should not fail if directory already exists", async () => {
579
+ const commandsDir = await ensureCommandsDirectory(TEST_DIR);
580
+ // Call again
581
+ const commandsDir2 = await ensureCommandsDirectory(TEST_DIR);
582
+
583
+ expect(commandsDir).toBe(commandsDir2);
584
+ });
585
+ });
586
+
587
+ describe("getCommandTemplate", () => {
588
+ it("should return valid template string", () => {
589
+ const template = getCommandTemplate();
590
+
591
+ expect(template).toContain("export default");
592
+ expect(template).toContain("name:");
593
+ expect(template).toContain("description:");
594
+ expect(template).toContain("execute:");
595
+ expect(template).toContain("/my-command");
596
+ });
597
+ });