@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,709 @@
1
+ /**
2
+ * Custom Agents CLI Command Tests (T024)
3
+ *
4
+ * Integration tests for custom agents CLI commands:
5
+ * - list (T020)
6
+ * - create (T021)
7
+ * - validate (T022)
8
+ * - info (T023)
9
+ * - export (T020a)
10
+ * - import (T020b)
11
+ *
12
+ * @module cli/commands/custom-agents/__tests__
13
+ */
14
+
15
+ import * as fs from "node:fs/promises";
16
+ import * as os from "node:os";
17
+ import * as path from "node:path";
18
+
19
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
20
+
21
+ import type {
22
+ CommandContext,
23
+ CommandError,
24
+ CommandInteractive,
25
+ CommandResult,
26
+ CommandSuccess,
27
+ ParsedArgs,
28
+ } from "../../types.js";
29
+ import { handleCreate } from "../create.js";
30
+ import { handleExport } from "../export.js";
31
+ import { handleImport } from "../import.js";
32
+ import { customAgentsCommand, executeCustomAgents, getCustomAgentsHelp } from "../index.js";
33
+ import { handleInfo } from "../info.js";
34
+ import { handleList, type ListJsonOutput } from "../list.js";
35
+ import { handleValidate } from "../validate.js";
36
+
37
+ // =============================================================================
38
+ // Test Setup
39
+ // =============================================================================
40
+
41
+ const TEST_DIR = path.join(os.tmpdir(), "vellum-custom-agents-test");
42
+ const AGENTS_DIR = path.join(TEST_DIR, ".vellum", "agents");
43
+
44
+ /**
45
+ * Type assertion helpers for CommandResult
46
+ */
47
+ function assertSuccess(result: CommandResult): asserts result is CommandSuccess {
48
+ expect(result.kind).toBe("success");
49
+ }
50
+
51
+ function assertError(result: CommandResult): asserts result is CommandError {
52
+ expect(result.kind).toBe("error");
53
+ }
54
+
55
+ function assertInteractive(result: CommandResult): asserts result is CommandInteractive {
56
+ expect(result.kind).toBe("interactive");
57
+ }
58
+
59
+ /**
60
+ * Valid agent YAML content
61
+ */
62
+ const VALID_AGENT_YAML = `slug: test-agent
63
+ name: "Test Agent"
64
+ mode: code
65
+ description: "A test agent for unit tests"
66
+ icon: "🧪"
67
+ tags:
68
+ - test
69
+ - example
70
+ `;
71
+
72
+ /**
73
+ * Valid agent Markdown content
74
+ */
75
+ const VALID_AGENT_MD = `---
76
+ slug: test-agent-md
77
+ name: "Test Agent MD"
78
+ mode: code
79
+ description: "A test agent in markdown format"
80
+ icon: "📝"
81
+ ---
82
+
83
+ # Test Agent
84
+
85
+ You are a test agent.
86
+
87
+ ## Instructions
88
+
89
+ Help with testing.
90
+ `;
91
+
92
+ /**
93
+ * Invalid agent (missing required fields)
94
+ */
95
+ const INVALID_AGENT_YAML = `name: "Invalid Agent"
96
+ # Missing slug
97
+ `;
98
+
99
+ /**
100
+ * Create mock CommandContext
101
+ */
102
+ function createMockContext(overrides: Partial<ParsedArgs> = {}): CommandContext {
103
+ return {
104
+ session: {
105
+ id: "test-session",
106
+ provider: "anthropic",
107
+ cwd: TEST_DIR,
108
+ },
109
+ credentials: {
110
+ resolve: vi.fn(),
111
+ store: vi.fn(),
112
+ delete: vi.fn(),
113
+ list: vi.fn(),
114
+ } as unknown as CommandContext["credentials"],
115
+ toolRegistry: {
116
+ get: vi.fn(),
117
+ list: vi.fn(),
118
+ } as unknown as CommandContext["toolRegistry"],
119
+ parsedArgs: {
120
+ command: overrides.command ?? "custom-agents",
121
+ positional: overrides.positional ?? [],
122
+ named: overrides.named ?? {},
123
+ raw: overrides.raw ?? "/custom-agents",
124
+ },
125
+ emit: vi.fn(),
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Write test agent file
131
+ */
132
+ async function writeTestAgent(filename: string, content: string): Promise<string> {
133
+ const filePath = path.join(AGENTS_DIR, filename);
134
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
135
+ await fs.writeFile(filePath, content, "utf-8");
136
+ return filePath;
137
+ }
138
+
139
+ // =============================================================================
140
+ // Setup/Teardown
141
+ // =============================================================================
142
+
143
+ beforeEach(async () => {
144
+ // Create test directory
145
+ await fs.mkdir(AGENTS_DIR, { recursive: true });
146
+ // Mock cwd
147
+ vi.spyOn(process, "cwd").mockReturnValue(TEST_DIR);
148
+ });
149
+
150
+ afterEach(async () => {
151
+ // Clean up test directory
152
+ try {
153
+ await fs.rm(TEST_DIR, { recursive: true, force: true });
154
+ } catch {
155
+ // Ignore errors during cleanup
156
+ }
157
+ vi.restoreAllMocks();
158
+ });
159
+
160
+ // =============================================================================
161
+ // T020: Command Definition Tests
162
+ // =============================================================================
163
+
164
+ describe("customAgentsCommand", () => {
165
+ describe("command definition", () => {
166
+ it("should have correct name and aliases", () => {
167
+ expect(customAgentsCommand.name).toBe("custom-agents");
168
+ expect(customAgentsCommand.aliases).toContain("ca");
169
+ expect(customAgentsCommand.aliases).toContain("custom-agent");
170
+ });
171
+
172
+ it("should be a builtin config command", () => {
173
+ expect(customAgentsCommand.kind).toBe("builtin");
174
+ expect(customAgentsCommand.category).toBe("config");
175
+ });
176
+
177
+ it("should define subcommand positional argument", () => {
178
+ const subArg = customAgentsCommand.positionalArgs?.find((a) => a.name === "subcommand");
179
+ expect(subArg).toBeDefined();
180
+ expect(subArg?.required).toBe(false);
181
+ });
182
+
183
+ it("should define expected named arguments", () => {
184
+ const namedArgs = customAgentsCommand.namedArgs ?? [];
185
+ expect(namedArgs.some((a) => a.name === "json")).toBe(true);
186
+ expect(namedArgs.some((a) => a.name === "global")).toBe(true);
187
+ expect(namedArgs.some((a) => a.name === "local")).toBe(true);
188
+ expect(namedArgs.some((a) => a.name === "template")).toBe(true);
189
+ expect(namedArgs.some((a) => a.name === "strict")).toBe(true);
190
+ expect(namedArgs.some((a) => a.name === "show-prompt")).toBe(true);
191
+ expect(namedArgs.some((a) => a.name === "output")).toBe(true);
192
+ expect(namedArgs.some((a) => a.name === "format")).toBe(true);
193
+ });
194
+ });
195
+
196
+ describe("help text", () => {
197
+ it("should show help when no subcommand provided", async () => {
198
+ const ctx = createMockContext({ positional: [] });
199
+ const result = await executeCustomAgents(ctx);
200
+
201
+ assertSuccess(result);
202
+ expect(result.message).toContain("Custom Agents Commands");
203
+ expect(result.message).toContain("list");
204
+ expect(result.message).toContain("create");
205
+ expect(result.message).toContain("validate");
206
+ expect(result.message).toContain("info");
207
+ expect(result.message).toContain("export");
208
+ expect(result.message).toContain("import");
209
+ });
210
+
211
+ it("should return same help from getCustomAgentsHelp", () => {
212
+ const help = getCustomAgentsHelp();
213
+ expect(help).toContain("Custom Agents Commands");
214
+ });
215
+ });
216
+ });
217
+
218
+ // =============================================================================
219
+ // T020: List Command Tests
220
+ // =============================================================================
221
+
222
+ describe("handleList", () => {
223
+ it("should return success with empty list when no agents", async () => {
224
+ const result = await handleList({});
225
+
226
+ assertSuccess(result);
227
+ expect(result.message).toContain("(none)");
228
+ });
229
+
230
+ it("should list agents grouped by scope", async () => {
231
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
232
+
233
+ const result = await handleList({});
234
+
235
+ assertSuccess(result);
236
+ expect(result.message).toContain("test-agent");
237
+ expect(result.message).toContain("Project Agents");
238
+ });
239
+
240
+ it("should output JSON when --json flag is set", async () => {
241
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
242
+
243
+ const result = await handleList({ json: true });
244
+
245
+ assertSuccess(result);
246
+ const json = JSON.parse(result.message ?? "{}") as ListJsonOutput;
247
+ expect(json.success).toBe(true);
248
+ expect(json.agents.project).toBeDefined();
249
+ expect(json.agents.user).toBeDefined();
250
+ expect(json.agents.system).toBeDefined();
251
+ });
252
+
253
+ it("should filter to project scope with --local flag", async () => {
254
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
255
+
256
+ const result = await handleList({ local: true });
257
+
258
+ assertSuccess(result);
259
+ expect(result.message).toContain("Project Agents");
260
+ expect(result.message).not.toContain("User Agents");
261
+ expect(result.message).not.toContain("System Agents");
262
+ });
263
+
264
+ it("should filter to user scope with --global flag", async () => {
265
+ const result = await handleList({ global: true });
266
+
267
+ assertSuccess(result);
268
+ expect(result.message).toContain("User Agents");
269
+ expect(result.message).not.toContain("Project Agents");
270
+ });
271
+ });
272
+
273
+ // =============================================================================
274
+ // T021: Create Command Tests
275
+ // =============================================================================
276
+
277
+ describe("handleCreate", () => {
278
+ it("should require slug when no-interactive", async () => {
279
+ const result = await handleCreate(undefined, { noInteractive: true });
280
+
281
+ assertError(result);
282
+ expect(result.code).toBe("MISSING_ARGUMENT");
283
+ });
284
+
285
+ it("should reject invalid slug", async () => {
286
+ const result = await handleCreate("Invalid_Slug!", {});
287
+
288
+ assertError(result);
289
+ expect(result.code).toBe("INVALID_ARGUMENT");
290
+ });
291
+
292
+ it("should create agent with basic template", async () => {
293
+ const result = await handleCreate("my-test-agent", {
294
+ template: "basic",
295
+ });
296
+
297
+ assertSuccess(result);
298
+ expect(result.message).toContain("Created agent");
299
+
300
+ // Verify file was created
301
+ const filePath = path.join(AGENTS_DIR, "my-test-agent.md");
302
+ const content = await fs.readFile(filePath, "utf-8");
303
+ expect(content).toContain("slug: my-test-agent");
304
+ expect(content).toContain('name: "My Test Agent"');
305
+ });
306
+
307
+ it("should create agent with advanced template", async () => {
308
+ const result = await handleCreate("advanced-agent", {
309
+ template: "advanced",
310
+ });
311
+
312
+ assertSuccess(result);
313
+
314
+ const filePath = path.join(AGENTS_DIR, "advanced-agent.md");
315
+ const content = await fs.readFile(filePath, "utf-8");
316
+ expect(content).toContain("toolGroups:");
317
+ expect(content).toContain("restrictions:");
318
+ });
319
+
320
+ it("should create agent with orchestrator template", async () => {
321
+ const result = await handleCreate("orchestrator-agent", {
322
+ template: "orchestrator",
323
+ });
324
+
325
+ assertSuccess(result);
326
+
327
+ const filePath = path.join(AGENTS_DIR, "orchestrator-agent.md");
328
+ const content = await fs.readFile(filePath, "utf-8");
329
+ expect(content).toContain("coordination:");
330
+ expect(content).toContain("level: orchestrator");
331
+ });
332
+
333
+ it("should reject duplicate slug", async () => {
334
+ // Create first agent
335
+ await handleCreate("duplicate-agent", {});
336
+
337
+ // Try to create again
338
+ const result = await handleCreate("duplicate-agent", {});
339
+
340
+ assertError(result);
341
+ expect(result.code).toBe("OPERATION_NOT_ALLOWED");
342
+ expect(result.message).toContain("already exists");
343
+ });
344
+
345
+ it("should reject unknown template", async () => {
346
+ const result = await handleCreate("my-agent", {
347
+ template: "unknown-template",
348
+ });
349
+
350
+ assertError(result);
351
+ expect(result.code).toBe("INVALID_ARGUMENT");
352
+ expect(result.message).toContain("Unknown template");
353
+ });
354
+ });
355
+
356
+ // =============================================================================
357
+ // T022: Validate Command Tests
358
+ // =============================================================================
359
+
360
+ describe("handleValidate", () => {
361
+ it("should return success message when no agents to validate", async () => {
362
+ const result = await handleValidate({});
363
+
364
+ assertSuccess(result);
365
+ expect(result.message).toContain("No custom agents found");
366
+ });
367
+
368
+ it("should validate all agents", async () => {
369
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
370
+
371
+ const result = await handleValidate({});
372
+
373
+ assertSuccess(result);
374
+ expect(result.message).toContain("valid");
375
+ expect(result.message).toContain("test-agent");
376
+ });
377
+
378
+ it("should validate specific agent by slug", async () => {
379
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
380
+
381
+ const result = await handleValidate({ target: "test-agent" });
382
+
383
+ assertSuccess(result);
384
+ expect(result.message).toContain("test-agent");
385
+ });
386
+
387
+ it("should validate specific agent by file path", async () => {
388
+ const filePath = await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
389
+
390
+ const result = await handleValidate({ target: filePath });
391
+
392
+ assertSuccess(result);
393
+ });
394
+
395
+ it("should report errors for invalid agent", async () => {
396
+ const filePath = await writeTestAgent("invalid-agent.yaml", INVALID_AGENT_YAML);
397
+
398
+ // Validate the specific file to catch parse errors
399
+ const result = await handleValidate({ target: filePath });
400
+
401
+ // Should fail validation due to missing slug
402
+ assertError(result);
403
+ expect(result.message).toContain("invalid");
404
+ });
405
+
406
+ it("should fail in strict mode with warnings", async () => {
407
+ // Create agent without description (generates warning)
408
+ const agentWithoutDesc = `slug: minimal-agent
409
+ name: "Minimal Agent"
410
+ `;
411
+ await writeTestAgent("minimal.yaml", agentWithoutDesc);
412
+
413
+ const result = await handleValidate({ strict: true });
414
+
415
+ // Should fail due to warnings in strict mode
416
+ assertError(result);
417
+ expect(result.message).toContain("warning");
418
+ });
419
+
420
+ it("should return error for non-existent agent", async () => {
421
+ const result = await handleValidate({ target: "non-existent" });
422
+
423
+ assertError(result);
424
+ expect(result.code).toBe("RESOURCE_NOT_FOUND");
425
+ });
426
+ });
427
+
428
+ // =============================================================================
429
+ // T023: Info Command Tests
430
+ // =============================================================================
431
+
432
+ describe("handleInfo", () => {
433
+ it("should require slug", async () => {
434
+ const result = await handleInfo(undefined, {});
435
+
436
+ assertError(result);
437
+ expect(result.code).toBe("MISSING_ARGUMENT");
438
+ });
439
+
440
+ it("should show agent info", async () => {
441
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
442
+
443
+ const result = await handleInfo("test-agent", {});
444
+
445
+ assertSuccess(result);
446
+ expect(result.message).toContain("Test Agent");
447
+ expect(result.message).toContain("test-agent");
448
+ expect(result.message).toContain("code");
449
+ });
450
+
451
+ it("should output JSON when --json flag is set", async () => {
452
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
453
+
454
+ const result = await handleInfo("test-agent", { json: true });
455
+
456
+ assertSuccess(result);
457
+ const json = JSON.parse(result.message ?? "{}");
458
+ expect(json.success).toBe(true);
459
+ expect(json.agent.slug).toBe("test-agent");
460
+ expect(json.agent.name).toBe("Test Agent");
461
+ });
462
+
463
+ it("should show full system prompt with --show-prompt", async () => {
464
+ await writeTestAgent("test-agent.md", VALID_AGENT_MD);
465
+
466
+ const result = await handleInfo("test-agent-md", { showPrompt: true });
467
+
468
+ assertSuccess(result);
469
+ expect(result.message).toContain("You are a test agent");
470
+ });
471
+
472
+ it("should return error for non-existent agent", async () => {
473
+ const result = await handleInfo("non-existent", {});
474
+
475
+ assertError(result);
476
+ expect(result.code).toBe("RESOURCE_NOT_FOUND");
477
+ });
478
+ });
479
+
480
+ // =============================================================================
481
+ // T020a: Export Command Tests
482
+ // =============================================================================
483
+
484
+ describe("handleExport", () => {
485
+ it("should require slug", async () => {
486
+ const result = await handleExport(undefined, {});
487
+
488
+ assertError(result);
489
+ expect(result.code).toBe("MISSING_ARGUMENT");
490
+ });
491
+
492
+ it("should export agent to stdout as YAML by default", async () => {
493
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
494
+
495
+ const result = await handleExport("test-agent", {});
496
+
497
+ assertSuccess(result);
498
+ expect(result.message).toContain("slug: test-agent");
499
+ expect(result.message).toContain("name: Test Agent");
500
+ });
501
+
502
+ it("should export agent as JSON", async () => {
503
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
504
+
505
+ const result = await handleExport("test-agent", { format: "json" });
506
+
507
+ assertSuccess(result);
508
+ expect(result.message).toContain('"slug": "test-agent"');
509
+ });
510
+
511
+ it("should export agent to file", async () => {
512
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
513
+ const outputPath = path.join(TEST_DIR, "exported.yaml");
514
+
515
+ const result = await handleExport("test-agent", { output: outputPath });
516
+
517
+ assertSuccess(result);
518
+ expect(result.message).toContain("Exported");
519
+
520
+ // Verify file was written
521
+ const content = await fs.readFile(outputPath, "utf-8");
522
+ expect(content).toContain("slug: test-agent");
523
+ });
524
+
525
+ it("should return error for non-existent agent", async () => {
526
+ const result = await handleExport("non-existent", {});
527
+
528
+ assertError(result);
529
+ expect(result.code).toBe("RESOURCE_NOT_FOUND");
530
+ });
531
+
532
+ it("should reject invalid format", async () => {
533
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
534
+
535
+ const result = await handleExport("test-agent", {
536
+ format: "xml" as "yaml",
537
+ });
538
+
539
+ assertError(result);
540
+ expect(result.code).toBe("INVALID_ARGUMENT");
541
+ });
542
+ });
543
+
544
+ // =============================================================================
545
+ // T020b: Import Command Tests
546
+ // =============================================================================
547
+
548
+ describe("handleImport", () => {
549
+ it("should require file path", async () => {
550
+ const result = await handleImport({ file: "" });
551
+
552
+ assertError(result);
553
+ expect(result.code).toBe("MISSING_ARGUMENT");
554
+ });
555
+
556
+ it("should return error for non-existent file", async () => {
557
+ const result = await handleImport({ file: "/non/existent/file.yaml" });
558
+
559
+ assertError(result);
560
+ expect(result.code).toBe("FILE_NOT_FOUND");
561
+ });
562
+
563
+ it("should import valid YAML agent", async () => {
564
+ // Write source file outside agents directory
565
+ const sourceFile = path.join(TEST_DIR, "import-source.yaml");
566
+ await fs.writeFile(sourceFile, VALID_AGENT_YAML, "utf-8");
567
+
568
+ const result = await handleImport({ file: sourceFile });
569
+
570
+ assertSuccess(result);
571
+ expect(result.message).toContain("Imported");
572
+
573
+ // Verify file was created in agents directory
574
+ const destPath = path.join(AGENTS_DIR, "test-agent.md");
575
+ const exists = await fs
576
+ .access(destPath)
577
+ .then(() => true)
578
+ .catch(() => false);
579
+ expect(exists).toBe(true);
580
+ });
581
+
582
+ it("should import valid Markdown agent", async () => {
583
+ const sourceFile = path.join(TEST_DIR, "import-source.md");
584
+ await fs.writeFile(sourceFile, VALID_AGENT_MD, "utf-8");
585
+
586
+ const result = await handleImport({ file: sourceFile });
587
+
588
+ assertSuccess(result);
589
+ expect(result.message).toContain("Imported");
590
+ });
591
+
592
+ it("should reject invalid agent file", async () => {
593
+ const sourceFile = path.join(TEST_DIR, "invalid.yaml");
594
+ await fs.writeFile(sourceFile, INVALID_AGENT_YAML, "utf-8");
595
+
596
+ const result = await handleImport({ file: sourceFile });
597
+
598
+ assertError(result);
599
+ expect(result.code).toBe("INVALID_ARGUMENT");
600
+ expect(result.message.toLowerCase()).toContain("validation failed");
601
+ });
602
+
603
+ it("should prompt for confirmation on existing agent", async () => {
604
+ // Create existing agent
605
+ await writeTestAgent("test-agent.yaml", VALID_AGENT_YAML);
606
+
607
+ // Try to import same slug
608
+ const sourceFile = path.join(TEST_DIR, "import-source.yaml");
609
+ await fs.writeFile(sourceFile, VALID_AGENT_YAML, "utf-8");
610
+
611
+ const result = await handleImport({ file: sourceFile });
612
+
613
+ // Should return interactive prompt
614
+ assertInteractive(result);
615
+ expect(result.prompt.inputType).toBe("confirm");
616
+ expect(result.prompt.message).toContain("already exists");
617
+ });
618
+ });
619
+
620
+ // =============================================================================
621
+ // Subcommand Routing Tests
622
+ // =============================================================================
623
+
624
+ describe("executeCustomAgents routing", () => {
625
+ it("should route list subcommand", async () => {
626
+ const ctx = createMockContext({
627
+ positional: ["list"],
628
+ named: { json: false },
629
+ });
630
+
631
+ const result = await executeCustomAgents(ctx);
632
+
633
+ assertSuccess(result);
634
+ });
635
+
636
+ it("should route create subcommand with slug", async () => {
637
+ const ctx = createMockContext({
638
+ positional: ["create", "new-agent"],
639
+ named: { template: "basic" },
640
+ });
641
+
642
+ const result = await executeCustomAgents(ctx);
643
+
644
+ assertSuccess(result);
645
+ expect(result.message).toContain("Created agent");
646
+ });
647
+
648
+ it("should route validate subcommand", async () => {
649
+ const ctx = createMockContext({
650
+ positional: ["validate"],
651
+ named: {},
652
+ });
653
+
654
+ const result = await executeCustomAgents(ctx);
655
+
656
+ assertSuccess(result);
657
+ });
658
+
659
+ it("should route info subcommand", async () => {
660
+ const ctx = createMockContext({
661
+ positional: ["info"],
662
+ named: {},
663
+ });
664
+
665
+ const result = await executeCustomAgents(ctx);
666
+
667
+ // Should error because slug is required
668
+ assertError(result);
669
+ expect(result.code).toBe("MISSING_ARGUMENT");
670
+ });
671
+
672
+ it("should route export subcommand", async () => {
673
+ const ctx = createMockContext({
674
+ positional: ["export"],
675
+ named: {},
676
+ });
677
+
678
+ const result = await executeCustomAgents(ctx);
679
+
680
+ // Should error because slug is required
681
+ assertError(result);
682
+ expect(result.code).toBe("MISSING_ARGUMENT");
683
+ });
684
+
685
+ it("should route import subcommand", async () => {
686
+ const ctx = createMockContext({
687
+ positional: ["import"],
688
+ named: {},
689
+ });
690
+
691
+ const result = await executeCustomAgents(ctx);
692
+
693
+ // Should error because file is required
694
+ assertError(result);
695
+ expect(result.code).toBe("MISSING_ARGUMENT");
696
+ });
697
+
698
+ it("should show help for unknown subcommand", async () => {
699
+ const ctx = createMockContext({
700
+ positional: ["unknown"],
701
+ named: {},
702
+ });
703
+
704
+ const result = await executeCustomAgents(ctx);
705
+
706
+ assertSuccess(result);
707
+ expect(result.message).toContain("Custom Agents Commands");
708
+ });
709
+ });