@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.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/__fixtures__/responses/code-generation.json +42 -0
- package/__fixtures__/responses/error-response.json +20 -0
- package/__fixtures__/responses/hello-world.json +32 -0
- package/dist/auth-6MCXESOH.js +26 -0
- package/dist/chunk-SECXJGWA.js +597 -0
- package/dist/index.js +34023 -0
- package/package.json +67 -0
- package/src/__tests__/commands.e2e.test.ts +728 -0
- package/src/__tests__/credentials.test.ts +713 -0
- package/src/__tests__/mode-e2e.test.ts +391 -0
- package/src/__tests__/tui-integration.test.tsx +1271 -0
- package/src/agents/__tests__/task-persistence.test.ts +235 -0
- package/src/agents/commands/delegate.ts +240 -0
- package/src/agents/commands/index.ts +10 -0
- package/src/agents/commands/resume.ts +335 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/task-persistence.ts +272 -0
- package/src/agents/task-resumption.ts +242 -0
- package/src/app.tsx +4737 -0
- package/src/commands/__tests__/.gitkeep +1 -0
- package/src/commands/__tests__/agents.test.ts +606 -0
- package/src/commands/__tests__/auth.test.ts +626 -0
- package/src/commands/__tests__/autocomplete.test.ts +683 -0
- package/src/commands/__tests__/batch.test.ts +287 -0
- package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
- package/src/commands/__tests__/completion.test.ts +238 -0
- package/src/commands/__tests__/core.test.ts +363 -0
- package/src/commands/__tests__/executor.test.ts +496 -0
- package/src/commands/__tests__/exit-codes.test.ts +220 -0
- package/src/commands/__tests__/init.test.ts +243 -0
- package/src/commands/__tests__/language.test.ts +353 -0
- package/src/commands/__tests__/mode-cli.test.ts +667 -0
- package/src/commands/__tests__/model.test.ts +277 -0
- package/src/commands/__tests__/parser.test.ts +493 -0
- package/src/commands/__tests__/performance.bench.ts +380 -0
- package/src/commands/__tests__/registry.test.ts +534 -0
- package/src/commands/__tests__/resume.test.ts +449 -0
- package/src/commands/__tests__/security.test.ts +845 -0
- package/src/commands/__tests__/stream-json.test.ts +372 -0
- package/src/commands/__tests__/user-commands.test.ts +597 -0
- package/src/commands/adapters.ts +267 -0
- package/src/commands/agent.ts +395 -0
- package/src/commands/agents/generate.ts +506 -0
- package/src/commands/agents/index.ts +272 -0
- package/src/commands/agents/show.ts +271 -0
- package/src/commands/agents/validate.ts +387 -0
- package/src/commands/auth.ts +883 -0
- package/src/commands/autocomplete.ts +480 -0
- package/src/commands/batch/command.ts +388 -0
- package/src/commands/batch/executor.ts +361 -0
- package/src/commands/batch/index.ts +12 -0
- package/src/commands/commit.ts +235 -0
- package/src/commands/completion/index.ts +371 -0
- package/src/commands/condense.ts +191 -0
- package/src/commands/config.ts +344 -0
- package/src/commands/context-provider.ts +173 -0
- package/src/commands/copy.ts +329 -0
- package/src/commands/core/clear.ts +38 -0
- package/src/commands/core/exit.ts +43 -0
- package/src/commands/core/help.ts +354 -0
- package/src/commands/core/index.ts +15 -0
- package/src/commands/cost.ts +179 -0
- package/src/commands/credentials.tsx +618 -0
- package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
- package/src/commands/custom-agents/create.ts +377 -0
- package/src/commands/custom-agents/export.ts +135 -0
- package/src/commands/custom-agents/import.ts +199 -0
- package/src/commands/custom-agents/index.ts +372 -0
- package/src/commands/custom-agents/info.ts +318 -0
- package/src/commands/custom-agents/list.ts +267 -0
- package/src/commands/custom-agents/validate.ts +388 -0
- package/src/commands/diff-mode.ts +241 -0
- package/src/commands/env.ts +53 -0
- package/src/commands/executor.ts +579 -0
- package/src/commands/exit-codes.ts +202 -0
- package/src/commands/index.ts +701 -0
- package/src/commands/init/index.ts +15 -0
- package/src/commands/init/prompts.ts +366 -0
- package/src/commands/init/templates/commands-readme.md +80 -0
- package/src/commands/init/templates/example-command.md +79 -0
- package/src/commands/init/templates/example-skill.md +168 -0
- package/src/commands/init/templates/example-workflow.md +101 -0
- package/src/commands/init/templates/prompts-readme.md +52 -0
- package/src/commands/init/templates/rules-readme.md +63 -0
- package/src/commands/init/templates/skills-readme.md +83 -0
- package/src/commands/init/templates/workflows-readme.md +94 -0
- package/src/commands/init.ts +391 -0
- package/src/commands/install.ts +90 -0
- package/src/commands/language.ts +191 -0
- package/src/commands/loaders/.gitkeep +1 -0
- package/src/commands/lsp.ts +199 -0
- package/src/commands/markdown-commands.ts +253 -0
- package/src/commands/mcp.ts +588 -0
- package/src/commands/memory/export.ts +341 -0
- package/src/commands/memory/index.ts +148 -0
- package/src/commands/memory/list.ts +261 -0
- package/src/commands/memory/search.ts +346 -0
- package/src/commands/memory/utils.ts +15 -0
- package/src/commands/metrics.ts +75 -0
- package/src/commands/migrate/index.ts +16 -0
- package/src/commands/migrate/prompts.ts +477 -0
- package/src/commands/mode.ts +331 -0
- package/src/commands/model.ts +298 -0
- package/src/commands/onboard.ts +205 -0
- package/src/commands/open.ts +169 -0
- package/src/commands/output/stream-json.ts +373 -0
- package/src/commands/parser/chain-parser.ts +370 -0
- package/src/commands/parser/index.ts +29 -0
- package/src/commands/parser/pipe-parser.ts +480 -0
- package/src/commands/parser.ts +588 -0
- package/src/commands/persistence.ts +355 -0
- package/src/commands/progress.ts +18 -0
- package/src/commands/prompt/index.ts +17 -0
- package/src/commands/prompt/validate.ts +621 -0
- package/src/commands/prompt-priority.ts +401 -0
- package/src/commands/registry.ts +374 -0
- package/src/commands/sandbox/index.ts +131 -0
- package/src/commands/security/index.ts +21 -0
- package/src/commands/security/input-sanitizer.ts +168 -0
- package/src/commands/security/permission-checker.ts +456 -0
- package/src/commands/security/sensitive-data.ts +350 -0
- package/src/commands/session/delete.ts +38 -0
- package/src/commands/session/export.ts +39 -0
- package/src/commands/session/index.ts +26 -0
- package/src/commands/session/list.ts +26 -0
- package/src/commands/session/resume.ts +562 -0
- package/src/commands/session/search.ts +434 -0
- package/src/commands/session/show.ts +26 -0
- package/src/commands/settings.ts +368 -0
- package/src/commands/setup.ts +23 -0
- package/src/commands/shell/index.ts +16 -0
- package/src/commands/shell/setup.ts +422 -0
- package/src/commands/shell-init.ts +50 -0
- package/src/commands/shell-integration/index.ts +194 -0
- package/src/commands/skill.ts +1220 -0
- package/src/commands/spec.ts +558 -0
- package/src/commands/status.ts +246 -0
- package/src/commands/theme.ts +211 -0
- package/src/commands/think.ts +551 -0
- package/src/commands/trust.ts +211 -0
- package/src/commands/tutorial.ts +522 -0
- package/src/commands/types.ts +512 -0
- package/src/commands/update.ts +274 -0
- package/src/commands/usage.ts +213 -0
- package/src/commands/user-commands.ts +630 -0
- package/src/commands/utils.ts +142 -0
- package/src/commands/vim.ts +152 -0
- package/src/commands/workflow.ts +257 -0
- package/src/components/header.tsx +25 -0
- package/src/components/input.tsx +25 -0
- package/src/components/message-list.tsx +32 -0
- package/src/components/status-bar.tsx +23 -0
- package/src/index.tsx +614 -0
- package/src/onboarding/__tests__/tutorial.test.ts +740 -0
- package/src/onboarding/index.ts +69 -0
- package/src/onboarding/tips/index.ts +9 -0
- package/src/onboarding/tips/tip-engine.ts +459 -0
- package/src/onboarding/tutorial/index.ts +88 -0
- package/src/onboarding/tutorial/lessons/basics.ts +151 -0
- package/src/onboarding/tutorial/lessons/index.ts +151 -0
- package/src/onboarding/tutorial/lessons/modes.ts +230 -0
- package/src/onboarding/tutorial/lessons/tools.ts +172 -0
- package/src/onboarding/tutorial/progress-tracker.ts +350 -0
- package/src/onboarding/tutorial/storage.ts +249 -0
- package/src/onboarding/tutorial/tutorial-system.ts +462 -0
- package/src/onboarding/tutorial/types.ts +310 -0
- package/src/orchestrator-singleton.ts +129 -0
- package/src/shutdown.ts +33 -0
- package/src/test/e2e/assertions.ts +267 -0
- package/src/test/e2e/fixtures.ts +204 -0
- package/src/test/e2e/harness.ts +575 -0
- package/src/test/e2e/index.ts +57 -0
- package/src/test/e2e/types.ts +228 -0
- package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
- package/src/test/fixtures/fake-response-loader.ts +314 -0
- package/src/test/fixtures/index.ts +20 -0
- package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
- package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
- package/src/tui/__tests__/mode-components.test.tsx +395 -0
- package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
- package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
- package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
- package/src/tui/adapters/agent-adapter.ts +1008 -0
- package/src/tui/adapters/index.ts +48 -0
- package/src/tui/adapters/message-adapter.ts +315 -0
- package/src/tui/adapters/persistence-bridge.ts +331 -0
- package/src/tui/adapters/session-adapter.ts +419 -0
- package/src/tui/buffered-stdout.ts +223 -0
- package/src/tui/components/AgentProgress.tsx +424 -0
- package/src/tui/components/Banner/AsciiArt.ts +160 -0
- package/src/tui/components/Banner/Banner.tsx +355 -0
- package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
- package/src/tui/components/Banner/ShimmerText.tsx +193 -0
- package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
- package/src/tui/components/Banner/index.ts +61 -0
- package/src/tui/components/Banner/useShimmer.ts +241 -0
- package/src/tui/components/ChatView.tsx +11 -0
- package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
- package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
- package/src/tui/components/Checkpoint/index.ts +19 -0
- package/src/tui/components/CostDisplay.tsx +226 -0
- package/src/tui/components/InitErrorBanner.tsx +122 -0
- package/src/tui/components/Input/Autocomplete.tsx +603 -0
- package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
- package/src/tui/components/Input/HighlightedText.tsx +236 -0
- package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
- package/src/tui/components/Input/TextInput.tsx +1002 -0
- package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
- package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
- package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
- package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
- package/src/tui/components/Input/highlight.ts +362 -0
- package/src/tui/components/Input/index.ts +36 -0
- package/src/tui/components/Input/slash-command-utils.ts +135 -0
- package/src/tui/components/Layout.tsx +432 -0
- package/src/tui/components/McpPanel.tsx +137 -0
- package/src/tui/components/MemoryPanel.tsx +448 -0
- package/src/tui/components/Messages/CodeBlock.tsx +527 -0
- package/src/tui/components/Messages/DiffView.tsx +679 -0
- package/src/tui/components/Messages/ImageReference.tsx +89 -0
- package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
- package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
- package/src/tui/components/Messages/MessageBubble.tsx +270 -0
- package/src/tui/components/Messages/MessageList.tsx +1719 -0
- package/src/tui/components/Messages/StreamingText.tsx +216 -0
- package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
- package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
- package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
- package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
- package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
- package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
- package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
- package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
- package/src/tui/components/Messages/index.ts +25 -0
- package/src/tui/components/ModeIndicator.tsx +177 -0
- package/src/tui/components/ModeSelector.tsx +216 -0
- package/src/tui/components/ModelSelector.tsx +339 -0
- package/src/tui/components/OnboardingWizard.tsx +670 -0
- package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
- package/src/tui/components/RateLimitIndicator.tsx +82 -0
- package/src/tui/components/ScreenReaderLayout.tsx +295 -0
- package/src/tui/components/SettingsPanel.tsx +643 -0
- package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
- package/src/tui/components/Sidebar/index.ts +9 -0
- package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
- package/src/tui/components/Status/index.ts +12 -0
- package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
- package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
- package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
- package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
- package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
- package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
- package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
- package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
- package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
- package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
- package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
- package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
- package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
- package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
- package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
- package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
- package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
- package/src/tui/components/StatusBar/index.ts +60 -0
- package/src/tui/components/TipBanner.tsx +115 -0
- package/src/tui/components/TodoItem.tsx +208 -0
- package/src/tui/components/TodoPanel.tsx +455 -0
- package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
- package/src/tui/components/Tools/OptionSelector.tsx +160 -0
- package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
- package/src/tui/components/Tools/ToolParams.tsx +483 -0
- package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
- package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
- package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
- package/src/tui/components/Tools/index.ts +21 -0
- package/src/tui/components/TrustPrompt.tsx +279 -0
- package/src/tui/components/UpdateBanner.tsx +166 -0
- package/src/tui/components/VimModeIndicator.tsx +112 -0
- package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
- package/src/tui/components/backtrack/index.ts +13 -0
- package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
- package/src/tui/components/common/CostWarning.tsx +294 -0
- package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
- package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
- package/src/tui/components/common/ErrorBoundary.tsx +140 -0
- package/src/tui/components/common/GradientText.tsx +224 -0
- package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
- package/src/tui/components/common/HotkeyHints.tsx +70 -0
- package/src/tui/components/common/MaxSizedBox.tsx +354 -0
- package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
- package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
- package/src/tui/components/common/ScrollIndicator.tsx +160 -0
- package/src/tui/components/common/Spinner.tsx +342 -0
- package/src/tui/components/common/StreamingIndicator.tsx +316 -0
- package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
- package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
- package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
- package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
- package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
- package/src/tui/components/common/VirtualizedList/index.ts +30 -0
- package/src/tui/components/common/VirtualizedList/types.ts +107 -0
- package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
- package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
- package/src/tui/components/common/index.ts +110 -0
- package/src/tui/components/index.ts +79 -0
- package/src/tui/components/session/CheckpointPanel.tsx +323 -0
- package/src/tui/components/session/RollbackDialog.tsx +169 -0
- package/src/tui/components/session/SessionItem.tsx +136 -0
- package/src/tui/components/session/SessionListPanel.tsx +252 -0
- package/src/tui/components/session/SessionPicker.tsx +449 -0
- package/src/tui/components/session/SessionPreview.tsx +240 -0
- package/src/tui/components/session/__tests__/session.test.tsx +408 -0
- package/src/tui/components/session/index.ts +28 -0
- package/src/tui/components/session/types.ts +116 -0
- package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
- package/src/tui/components/theme/index.ts +227 -0
- package/src/tui/components/theme/tokens.ts +484 -0
- package/src/tui/config/defaults.ts +134 -0
- package/src/tui/config/index.ts +17 -0
- package/src/tui/context/AnimationContext.tsx +284 -0
- package/src/tui/context/AppContext.tsx +349 -0
- package/src/tui/context/BracketedPasteContext.tsx +372 -0
- package/src/tui/context/LspContext.tsx +192 -0
- package/src/tui/context/McpContext.tsx +325 -0
- package/src/tui/context/MessagesContext.tsx +870 -0
- package/src/tui/context/OverflowContext.tsx +213 -0
- package/src/tui/context/RateLimitContext.tsx +108 -0
- package/src/tui/context/ResilienceContext.tsx +275 -0
- package/src/tui/context/RootProvider.tsx +136 -0
- package/src/tui/context/ScrollContext.tsx +331 -0
- package/src/tui/context/ToolsContext.tsx +702 -0
- package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
- package/src/tui/context/index.ts +140 -0
- package/src/tui/enterprise-integration.ts +282 -0
- package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
- package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
- package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
- package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
- package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
- package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
- package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
- package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
- package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
- package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
- package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
- package/src/tui/hooks/index.ts +252 -0
- package/src/tui/hooks/useAgentLoop.ts +712 -0
- package/src/tui/hooks/useAlternateBuffer.ts +398 -0
- package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
- package/src/tui/hooks/useBacktrack.ts +443 -0
- package/src/tui/hooks/useBracketedPaste.ts +104 -0
- package/src/tui/hooks/useCollapsible.ts +240 -0
- package/src/tui/hooks/useCopyMode.ts +382 -0
- package/src/tui/hooks/useCostSummary.ts +75 -0
- package/src/tui/hooks/useDesktopNotification.ts +414 -0
- package/src/tui/hooks/useDiffMode.ts +44 -0
- package/src/tui/hooks/useFileChangeStats.ts +110 -0
- package/src/tui/hooks/useFileSuggestions.ts +284 -0
- package/src/tui/hooks/useFlickerDetector.ts +250 -0
- package/src/tui/hooks/useGitStatus.ts +200 -0
- package/src/tui/hooks/useHotkeys.ts +579 -0
- package/src/tui/hooks/useImagePaste.ts +114 -0
- package/src/tui/hooks/useInputHighlight.ts +145 -0
- package/src/tui/hooks/useInputHistory.ts +246 -0
- package/src/tui/hooks/useKeyboardScroll.ts +209 -0
- package/src/tui/hooks/useLineBuffer.ts +356 -0
- package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
- package/src/tui/hooks/useModeController.ts +167 -0
- package/src/tui/hooks/useModeShortcuts.ts +196 -0
- package/src/tui/hooks/usePermissionHandler.ts +146 -0
- package/src/tui/hooks/usePersistence.ts +480 -0
- package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
- package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
- package/src/tui/hooks/useProviderStatus.ts +270 -0
- package/src/tui/hooks/useRateLimitStatus.ts +90 -0
- package/src/tui/hooks/useScreenReader.ts +315 -0
- package/src/tui/hooks/useScrollController.ts +450 -0
- package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
- package/src/tui/hooks/useSidebarPanelData.ts +115 -0
- package/src/tui/hooks/useSmoothScroll.ts +202 -0
- package/src/tui/hooks/useSnapshots.ts +300 -0
- package/src/tui/hooks/useStateAndRef.ts +50 -0
- package/src/tui/hooks/useTerminalSize.ts +206 -0
- package/src/tui/hooks/useToolApprovalController.ts +91 -0
- package/src/tui/hooks/useVim.ts +334 -0
- package/src/tui/hooks/useWorkspace.ts +56 -0
- package/src/tui/i18n/__tests__/init.test.ts +278 -0
- package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
- package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
- package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
- package/src/tui/i18n/index.ts +72 -0
- package/src/tui/i18n/init.ts +131 -0
- package/src/tui/i18n/language-config.ts +106 -0
- package/src/tui/i18n/locale-detection.ts +173 -0
- package/src/tui/i18n/settings-integration.ts +557 -0
- package/src/tui/i18n/tui-namespace.ts +538 -0
- package/src/tui/i18n/types.ts +312 -0
- package/src/tui/index.ts +43 -0
- package/src/tui/lsp-integration.ts +409 -0
- package/src/tui/metrics-integration.ts +366 -0
- package/src/tui/plugins.ts +383 -0
- package/src/tui/resilience.ts +342 -0
- package/src/tui/sandbox-integration.ts +317 -0
- package/src/tui/services/clipboard.ts +348 -0
- package/src/tui/services/fuzzy-search.ts +441 -0
- package/src/tui/services/index.ts +72 -0
- package/src/tui/services/markdown-renderer.ts +565 -0
- package/src/tui/services/open-external.ts +247 -0
- package/src/tui/services/syntax-highlighter.ts +483 -0
- package/src/tui/slash-commands.ts +12 -0
- package/src/tui/theme/index.ts +15 -0
- package/src/tui/theme/provider.tsx +206 -0
- package/src/tui/tip-integration.ts +300 -0
- package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
- package/src/tui/types/ink-extended.ts +87 -0
- package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
- package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
- package/src/tui/utils/__tests__/text-width.test.ts +158 -0
- package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
- package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
- package/src/tui/utils/bracketedPaste.ts +107 -0
- package/src/tui/utils/cursor-manager.ts +131 -0
- package/src/tui/utils/detectTerminal.ts +596 -0
- package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
- package/src/tui/utils/heightEstimator.ts +198 -0
- package/src/tui/utils/index.ts +91 -0
- package/src/tui/utils/isNarrowWidth.ts +52 -0
- package/src/tui/utils/stdoutGuard.ts +90 -0
- package/src/tui/utils/synchronized-update.ts +70 -0
- package/src/tui/utils/text-width.ts +225 -0
- package/src/tui/utils/textSanitizer.ts +225 -0
- package/src/tui/utils/textUtils.ts +114 -0
- package/src/tui/utils/ui-sizing.ts +192 -0
- package/src/tui-blessed/app.ts +160 -0
- package/src/tui-blessed/index.ts +2 -0
- package/src/tui-blessed/neo-blessed.d.ts +6 -0
- package/src/tui-blessed/test.ts +21 -0
- package/src/tui-blessed/types.ts +14 -0
- package/src/utils/icons.ts +130 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/resume-hint.ts +86 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +8 -0
- 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
|
+
});
|