@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,1220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill CLI Commands
|
|
3
|
+
*
|
|
4
|
+
* Provides commands for managing skills:
|
|
5
|
+
* - skill list: List all available skills
|
|
6
|
+
* - skill show: Show details of a specific skill
|
|
7
|
+
* - skill create: Create a new skill from template
|
|
8
|
+
* - skill validate: Validate skill(s)
|
|
9
|
+
*
|
|
10
|
+
* @module cli/commands/skill
|
|
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 { confirm, input, select } from "@inquirer/prompts";
|
|
18
|
+
import {
|
|
19
|
+
createSkillManager,
|
|
20
|
+
SkillDiscovery,
|
|
21
|
+
type SkillLocation,
|
|
22
|
+
SkillParser,
|
|
23
|
+
type SkillScan,
|
|
24
|
+
type SkillSource,
|
|
25
|
+
type SkillTrigger,
|
|
26
|
+
} from "@vellum/core";
|
|
27
|
+
import chalk from "chalk";
|
|
28
|
+
import { ICONS } from "../utils/icons.js";
|
|
29
|
+
import { EXIT_CODES } from "./exit-codes.js";
|
|
30
|
+
import type { CommandResult } from "./types.js";
|
|
31
|
+
import { error, success } from "./types.js";
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Types
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for skill list command
|
|
39
|
+
*/
|
|
40
|
+
export interface SkillListOptions {
|
|
41
|
+
/** Filter by source (workspace, user, global, builtin) */
|
|
42
|
+
source?: SkillSource;
|
|
43
|
+
/** Output as JSON */
|
|
44
|
+
json?: boolean;
|
|
45
|
+
/** Show full descriptions */
|
|
46
|
+
verbose?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for skill show command
|
|
51
|
+
*/
|
|
52
|
+
export interface SkillShowOptions {
|
|
53
|
+
/** Show full SKILL.md content */
|
|
54
|
+
content?: boolean;
|
|
55
|
+
/** Output as JSON */
|
|
56
|
+
json?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Options for skill create command
|
|
61
|
+
*/
|
|
62
|
+
export interface SkillCreateOptions {
|
|
63
|
+
/** Location to create skill (workspace, user, global) */
|
|
64
|
+
location?: SkillSource;
|
|
65
|
+
/** Non-interactive mode */
|
|
66
|
+
nonInteractive?: boolean;
|
|
67
|
+
/** Force overwrite if exists */
|
|
68
|
+
force?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Options for skill validate command
|
|
73
|
+
*/
|
|
74
|
+
export interface SkillValidateOptions {
|
|
75
|
+
/** Validate single skill by name */
|
|
76
|
+
skill?: string;
|
|
77
|
+
/** Treat warnings as errors */
|
|
78
|
+
strict?: boolean;
|
|
79
|
+
/** Output as JSON */
|
|
80
|
+
json?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* JSON output for skill list
|
|
85
|
+
*/
|
|
86
|
+
interface SkillListJson {
|
|
87
|
+
success: boolean;
|
|
88
|
+
skills: Array<{
|
|
89
|
+
name: string;
|
|
90
|
+
description: string;
|
|
91
|
+
source: SkillSource;
|
|
92
|
+
path: string;
|
|
93
|
+
version?: string;
|
|
94
|
+
tags: string[];
|
|
95
|
+
triggers: Array<{ type: string; pattern?: string }>;
|
|
96
|
+
}>;
|
|
97
|
+
total: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* JSON output for skill show
|
|
102
|
+
*/
|
|
103
|
+
interface SkillShowJson {
|
|
104
|
+
success: boolean;
|
|
105
|
+
skill: {
|
|
106
|
+
name: string;
|
|
107
|
+
description: string;
|
|
108
|
+
source: SkillSource;
|
|
109
|
+
path: string;
|
|
110
|
+
version?: string;
|
|
111
|
+
priority: number;
|
|
112
|
+
tags: string[];
|
|
113
|
+
dependencies: string[];
|
|
114
|
+
triggers: Array<{ type: string; pattern?: string }>;
|
|
115
|
+
content?: string;
|
|
116
|
+
sections?: {
|
|
117
|
+
rules?: string;
|
|
118
|
+
patterns?: string;
|
|
119
|
+
antiPatterns?: string;
|
|
120
|
+
examples?: string;
|
|
121
|
+
references?: string;
|
|
122
|
+
};
|
|
123
|
+
} | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validation result for a single skill
|
|
128
|
+
*/
|
|
129
|
+
interface SkillValidationResult {
|
|
130
|
+
name: string;
|
|
131
|
+
path: string;
|
|
132
|
+
valid: boolean;
|
|
133
|
+
errors: string[];
|
|
134
|
+
warnings: string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* JSON output for skill validate
|
|
139
|
+
*/
|
|
140
|
+
interface SkillValidateJson {
|
|
141
|
+
success: boolean;
|
|
142
|
+
results: SkillValidationResult[];
|
|
143
|
+
summary: {
|
|
144
|
+
total: number;
|
|
145
|
+
valid: number;
|
|
146
|
+
invalid: number;
|
|
147
|
+
warnings: number;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Skill Template
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Template for creating new skills
|
|
157
|
+
*/
|
|
158
|
+
const SKILL_TEMPLATE = `---
|
|
159
|
+
name: "{name}"
|
|
160
|
+
description: "{description}"
|
|
161
|
+
version: "1.0.0"
|
|
162
|
+
priority: 50
|
|
163
|
+
tags:
|
|
164
|
+
- custom
|
|
165
|
+
triggers:
|
|
166
|
+
- type: keyword
|
|
167
|
+
pattern: "{name}"
|
|
168
|
+
globs:
|
|
169
|
+
- "**/*.ts"
|
|
170
|
+
- "**/*.tsx"
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
# {name}
|
|
174
|
+
|
|
175
|
+
{description}
|
|
176
|
+
|
|
177
|
+
## Rules
|
|
178
|
+
|
|
179
|
+
<!-- Define the rules this skill enforces -->
|
|
180
|
+
|
|
181
|
+
- Rule 1: Description of rule
|
|
182
|
+
- Rule 2: Description of rule
|
|
183
|
+
|
|
184
|
+
## Patterns
|
|
185
|
+
|
|
186
|
+
<!-- Provide code patterns to follow -->
|
|
187
|
+
|
|
188
|
+
\`\`\`typescript
|
|
189
|
+
// Good pattern example
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
## Anti-Patterns
|
|
193
|
+
|
|
194
|
+
<!-- Provide patterns to avoid -->
|
|
195
|
+
|
|
196
|
+
\`\`\`typescript
|
|
197
|
+
// Anti-pattern example - DON'T do this
|
|
198
|
+
\`\`\`
|
|
199
|
+
|
|
200
|
+
## Examples
|
|
201
|
+
|
|
202
|
+
<!-- Provide usage examples -->
|
|
203
|
+
|
|
204
|
+
### Example 1
|
|
205
|
+
|
|
206
|
+
Description of the example.
|
|
207
|
+
|
|
208
|
+
## References
|
|
209
|
+
|
|
210
|
+
<!-- Link to external documentation -->
|
|
211
|
+
|
|
212
|
+
- [Reference Name](https://example.com)
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
// =============================================================================
|
|
216
|
+
// Helpers
|
|
217
|
+
// =============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get skill source path based on location type
|
|
221
|
+
*/
|
|
222
|
+
function getSkillSourcePath(location: SkillSource, workspacePath: string): string {
|
|
223
|
+
switch (location) {
|
|
224
|
+
case "workspace":
|
|
225
|
+
return path.join(workspacePath, ".vellum", "skills");
|
|
226
|
+
case "user":
|
|
227
|
+
return path.join(os.homedir(), ".vellum", "skills");
|
|
228
|
+
case "global":
|
|
229
|
+
return path.join(workspacePath, ".github", "skills");
|
|
230
|
+
default:
|
|
231
|
+
throw new Error(`Cannot create skills in ${location} location`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Ensure directory exists
|
|
237
|
+
*/
|
|
238
|
+
async function ensureDir(dirPath: string): Promise<void> {
|
|
239
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check if file exists
|
|
244
|
+
*/
|
|
245
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
246
|
+
try {
|
|
247
|
+
await fs.access(filePath);
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Format skill source with color
|
|
256
|
+
*/
|
|
257
|
+
function formatSource(source: SkillSource): string {
|
|
258
|
+
const colors: Record<SkillSource, (s: string) => string> = {
|
|
259
|
+
workspace: chalk.green,
|
|
260
|
+
user: chalk.blue,
|
|
261
|
+
global: chalk.yellow,
|
|
262
|
+
plugin: chalk.magenta,
|
|
263
|
+
builtin: chalk.gray,
|
|
264
|
+
};
|
|
265
|
+
const colorFn = colors[source] ?? chalk.white;
|
|
266
|
+
return colorFn(source);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Truncate string with ellipsis
|
|
271
|
+
*/
|
|
272
|
+
function truncate(str: string, maxLength: number): string {
|
|
273
|
+
if (str.length <= maxLength) return str;
|
|
274
|
+
return `${str.slice(0, maxLength - 3)}...`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// =============================================================================
|
|
278
|
+
// List Command (T034)
|
|
279
|
+
// =============================================================================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Execute skill list command
|
|
283
|
+
*/
|
|
284
|
+
export async function handleSkillList(options: SkillListOptions = {}): Promise<CommandResult> {
|
|
285
|
+
try {
|
|
286
|
+
const manager = createSkillManager({
|
|
287
|
+
loader: { discovery: { workspacePath: process.cwd() } },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await manager.initialize();
|
|
291
|
+
let skills = manager.getAllSkills();
|
|
292
|
+
|
|
293
|
+
// Filter by source if specified
|
|
294
|
+
if (options.source) {
|
|
295
|
+
skills = skills.filter((s: SkillScan) => s.source === options.source);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// JSON output
|
|
299
|
+
if (options.json) {
|
|
300
|
+
const output: SkillListJson = {
|
|
301
|
+
success: true,
|
|
302
|
+
skills: skills.map((s: SkillScan) => ({
|
|
303
|
+
name: s.name,
|
|
304
|
+
description: s.description,
|
|
305
|
+
source: s.source,
|
|
306
|
+
path: s.path,
|
|
307
|
+
version: s.version,
|
|
308
|
+
tags: s.tags,
|
|
309
|
+
triggers: s.triggers.map((t: SkillTrigger) => ({
|
|
310
|
+
type: t.type,
|
|
311
|
+
pattern: t.pattern,
|
|
312
|
+
})),
|
|
313
|
+
})),
|
|
314
|
+
total: skills.length,
|
|
315
|
+
};
|
|
316
|
+
return success(JSON.stringify(output, null, 2));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Table output
|
|
320
|
+
if (skills.length === 0) {
|
|
321
|
+
return success(chalk.yellow("No skills found."));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const lines: string[] = [];
|
|
325
|
+
lines.push(chalk.bold.cyan("\n📚 Available Skills\n"));
|
|
326
|
+
|
|
327
|
+
// Header
|
|
328
|
+
const nameWidth = 25;
|
|
329
|
+
const sourceWidth = 10;
|
|
330
|
+
const descWidth = options.verbose ? 60 : 40;
|
|
331
|
+
|
|
332
|
+
lines.push(
|
|
333
|
+
chalk.gray(
|
|
334
|
+
`${"Name".padEnd(nameWidth)} ${"Source".padEnd(sourceWidth)} ${"Description".padEnd(descWidth)}`
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
lines.push(chalk.gray("─".repeat(nameWidth + sourceWidth + descWidth + 2)));
|
|
338
|
+
|
|
339
|
+
// Rows
|
|
340
|
+
for (const skill of skills) {
|
|
341
|
+
const name = chalk.white(truncate(skill.name, nameWidth).padEnd(nameWidth));
|
|
342
|
+
const source = formatSource(skill.source).padEnd(sourceWidth + 10); // Account for ANSI codes
|
|
343
|
+
const desc = truncate(skill.description, descWidth);
|
|
344
|
+
|
|
345
|
+
lines.push(`${name} ${source} ${desc}`);
|
|
346
|
+
|
|
347
|
+
// Verbose mode: show triggers
|
|
348
|
+
if (options.verbose && skill.triggers.length > 0) {
|
|
349
|
+
for (const trigger of skill.triggers) {
|
|
350
|
+
const triggerStr =
|
|
351
|
+
trigger.type === "always"
|
|
352
|
+
? chalk.gray(" └─ always active")
|
|
353
|
+
: chalk.gray(` └─ ${trigger.type}: ${trigger.pattern}`);
|
|
354
|
+
lines.push(triggerStr);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
lines.push(chalk.gray(`\nTotal: ${skills.length} skill(s)`));
|
|
360
|
+
|
|
361
|
+
return success(lines.join("\n"));
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
364
|
+
return error("INTERNAL_ERROR", `Failed to list skills: ${message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// =============================================================================
|
|
369
|
+
// Show Command (T035)
|
|
370
|
+
// =============================================================================
|
|
371
|
+
|
|
372
|
+
/** Format skill data as JSON output */
|
|
373
|
+
function formatSkillShowJson(
|
|
374
|
+
scan: SkillScan,
|
|
375
|
+
loaded: Awaited<ReturnType<ReturnType<typeof createSkillManager>["loadSkill"]>> | null,
|
|
376
|
+
options: SkillShowOptions
|
|
377
|
+
): SkillShowJson {
|
|
378
|
+
return {
|
|
379
|
+
success: true,
|
|
380
|
+
skill: {
|
|
381
|
+
name: scan.name,
|
|
382
|
+
description: scan.description,
|
|
383
|
+
source: scan.source,
|
|
384
|
+
path: scan.path,
|
|
385
|
+
version: scan.version,
|
|
386
|
+
priority: scan.priority,
|
|
387
|
+
tags: scan.tags,
|
|
388
|
+
dependencies: scan.dependencies,
|
|
389
|
+
triggers: scan.triggers.map((t: SkillTrigger) => ({
|
|
390
|
+
type: t.type,
|
|
391
|
+
pattern: t.pattern,
|
|
392
|
+
})),
|
|
393
|
+
content: options.content ? loaded?.raw : undefined,
|
|
394
|
+
sections: loaded
|
|
395
|
+
? {
|
|
396
|
+
rules: loaded.rules || undefined,
|
|
397
|
+
patterns: loaded.patterns || undefined,
|
|
398
|
+
antiPatterns: loaded.antiPatterns || undefined,
|
|
399
|
+
examples: loaded.examples || undefined,
|
|
400
|
+
references: loaded.referencesSection || undefined,
|
|
401
|
+
}
|
|
402
|
+
: undefined,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Format skill metadata lines */
|
|
408
|
+
function formatSkillMetadataLines(scan: SkillScan): string[] {
|
|
409
|
+
const lines: string[] = [];
|
|
410
|
+
lines.push(chalk.bold.cyan(`\n📖 Skill: ${scan.name}\n`));
|
|
411
|
+
lines.push(`${chalk.white("Description:")} ${scan.description}`);
|
|
412
|
+
lines.push(`${chalk.white("Source:")} ${formatSource(scan.source)}`);
|
|
413
|
+
lines.push(`${chalk.white("Path:")} ${chalk.gray(scan.path)}`);
|
|
414
|
+
lines.push(`${chalk.white("Priority:")} ${scan.priority}`);
|
|
415
|
+
|
|
416
|
+
if (scan.version) {
|
|
417
|
+
lines.push(`${chalk.white("Version:")} ${scan.version}`);
|
|
418
|
+
}
|
|
419
|
+
if (scan.tags.length > 0) {
|
|
420
|
+
lines.push(`${chalk.white("Tags:")} ${scan.tags.map((t: string) => chalk.cyan(t)).join(", ")}`);
|
|
421
|
+
}
|
|
422
|
+
if (scan.dependencies.length > 0) {
|
|
423
|
+
lines.push(
|
|
424
|
+
chalk.white("Dependencies:") +
|
|
425
|
+
" " +
|
|
426
|
+
scan.dependencies.map((d: string) => chalk.yellow(d)).join(", ")
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
return lines;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Format skill triggers as lines */
|
|
433
|
+
function formatSkillTriggersLines(triggers: SkillTrigger[]): string[] {
|
|
434
|
+
const lines: string[] = [chalk.white("\nTriggers:")];
|
|
435
|
+
for (const trigger of triggers) {
|
|
436
|
+
if (trigger.type === "always") {
|
|
437
|
+
lines.push(chalk.gray(" • always active"));
|
|
438
|
+
} else {
|
|
439
|
+
lines.push(chalk.gray(` • ${trigger.type}: ${trigger.pattern}`));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return lines;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Format skill content/sections as lines */
|
|
446
|
+
function formatSkillContentLines(
|
|
447
|
+
loaded: Awaited<ReturnType<ReturnType<typeof createSkillManager>["loadSkill"]>> | null,
|
|
448
|
+
showContent: boolean
|
|
449
|
+
): string[] {
|
|
450
|
+
const lines: string[] = [];
|
|
451
|
+
if (showContent && loaded) {
|
|
452
|
+
lines.push(chalk.white("\n─── SKILL.md Content ───\n"));
|
|
453
|
+
lines.push(loaded.raw);
|
|
454
|
+
} else if (loaded) {
|
|
455
|
+
lines.push(chalk.white("\nSections:"));
|
|
456
|
+
if (loaded.rules) lines.push(chalk.gray(" • Rules (✓)"));
|
|
457
|
+
if (loaded.patterns) lines.push(chalk.gray(" • Patterns (✓)"));
|
|
458
|
+
if (loaded.antiPatterns) lines.push(chalk.gray(" • Anti-Patterns (✓)"));
|
|
459
|
+
if (loaded.examples) lines.push(chalk.gray(" • Examples (✓)"));
|
|
460
|
+
if (loaded.referencesSection) lines.push(chalk.gray(" • References (✓)"));
|
|
461
|
+
lines.push(chalk.gray("\nUse --content to show full SKILL.md content"));
|
|
462
|
+
}
|
|
463
|
+
return lines;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Execute skill show command
|
|
468
|
+
*/
|
|
469
|
+
export async function handleSkillShow(
|
|
470
|
+
name: string,
|
|
471
|
+
options: SkillShowOptions = {}
|
|
472
|
+
): Promise<CommandResult> {
|
|
473
|
+
try {
|
|
474
|
+
const manager = createSkillManager({
|
|
475
|
+
loader: { discovery: { workspacePath: process.cwd() } },
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
await manager.initialize();
|
|
479
|
+
const scan = manager.getSkill(name);
|
|
480
|
+
|
|
481
|
+
if (!scan) {
|
|
482
|
+
if (options.json) {
|
|
483
|
+
const output: SkillShowJson = { success: false, skill: null };
|
|
484
|
+
return error("RESOURCE_NOT_FOUND", JSON.stringify(output, null, 2));
|
|
485
|
+
}
|
|
486
|
+
return error("RESOURCE_NOT_FOUND", chalk.red(`Skill not found: ${name}`));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const loaded = await manager.loadSkill(name);
|
|
490
|
+
|
|
491
|
+
if (options.json) {
|
|
492
|
+
return success(JSON.stringify(formatSkillShowJson(scan, loaded, options), null, 2));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const lines: string[] = [
|
|
496
|
+
...formatSkillMetadataLines(scan),
|
|
497
|
+
...formatSkillTriggersLines(scan.triggers),
|
|
498
|
+
...formatSkillContentLines(loaded, options.content ?? false),
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
return success(lines.join("\n"));
|
|
502
|
+
} catch (err) {
|
|
503
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
504
|
+
return error("INTERNAL_ERROR", `Failed to show skill: ${message}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// =============================================================================
|
|
509
|
+
// Create Command (T036)
|
|
510
|
+
// =============================================================================
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Execute skill create command
|
|
514
|
+
*/
|
|
515
|
+
export async function handleSkillCreate(
|
|
516
|
+
name: string,
|
|
517
|
+
options: SkillCreateOptions = {}
|
|
518
|
+
): Promise<{ success: boolean; path?: string; error?: string; exitCode: number }> {
|
|
519
|
+
try {
|
|
520
|
+
const workspacePath = process.cwd();
|
|
521
|
+
let location = options.location;
|
|
522
|
+
|
|
523
|
+
// Interactive location selection
|
|
524
|
+
if (!location && !options.nonInteractive) {
|
|
525
|
+
console.log(chalk.bold.blue("\n🛠️ Create New Skill\n"));
|
|
526
|
+
|
|
527
|
+
location = await select({
|
|
528
|
+
message: "Where would you like to create the skill?",
|
|
529
|
+
choices: [
|
|
530
|
+
{ name: "Workspace (.vellum/skills/) - Project-specific", value: "workspace" as const },
|
|
531
|
+
{ name: "User (~/.vellum/skills/) - Available across projects", value: "user" as const },
|
|
532
|
+
{ name: "Global (.github/skills/) - Claude compatibility", value: "global" as const },
|
|
533
|
+
],
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Default to workspace
|
|
538
|
+
location = location ?? "workspace";
|
|
539
|
+
|
|
540
|
+
// Validate location
|
|
541
|
+
if (location === "builtin") {
|
|
542
|
+
console.error(chalk.red("Cannot create skills in builtin location"));
|
|
543
|
+
return { success: false, error: "Invalid location", exitCode: EXIT_CODES.ERROR };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Get target path
|
|
547
|
+
const skillsDir = getSkillSourcePath(location, workspacePath);
|
|
548
|
+
const skillDir = path.join(skillsDir, name);
|
|
549
|
+
const manifestPath = path.join(skillDir, "SKILL.md");
|
|
550
|
+
|
|
551
|
+
// Check if skill already exists
|
|
552
|
+
if ((await fileExists(manifestPath)) && !options.force) {
|
|
553
|
+
if (options.nonInteractive) {
|
|
554
|
+
console.error(chalk.red(`Skill already exists: ${name}. Use --force to overwrite.`));
|
|
555
|
+
return { success: false, error: "Skill already exists", exitCode: EXIT_CODES.ERROR };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const shouldOverwrite = await confirm({
|
|
559
|
+
message: `Skill "${name}" already exists at ${skillDir}. Overwrite?`,
|
|
560
|
+
default: false,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
if (!shouldOverwrite) {
|
|
564
|
+
console.log(chalk.gray("Aborted."));
|
|
565
|
+
return { success: false, error: "Aborted by user", exitCode: EXIT_CODES.SUCCESS };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Get description
|
|
570
|
+
let description = "A custom skill";
|
|
571
|
+
if (!options.nonInteractive) {
|
|
572
|
+
description = await input({
|
|
573
|
+
message: "Brief description of the skill:",
|
|
574
|
+
default: description,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Generate skill content from template
|
|
579
|
+
const content = SKILL_TEMPLATE.replace(/\{name\}/g, name).replace(
|
|
580
|
+
/\{description\}/g,
|
|
581
|
+
description
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
// Create directories and file
|
|
585
|
+
await ensureDir(skillDir);
|
|
586
|
+
await fs.writeFile(manifestPath, content, "utf-8");
|
|
587
|
+
|
|
588
|
+
// Create optional subdirectories
|
|
589
|
+
await ensureDir(path.join(skillDir, "scripts"));
|
|
590
|
+
await ensureDir(path.join(skillDir, "references"));
|
|
591
|
+
|
|
592
|
+
console.log(chalk.green(`\n${ICONS.success} Created skill: ${name}`));
|
|
593
|
+
console.log(chalk.gray(` Path: ${skillDir}`));
|
|
594
|
+
console.log(chalk.gray("\n Next steps:"));
|
|
595
|
+
console.log(chalk.gray(` 1. Edit ${manifestPath}`));
|
|
596
|
+
console.log(chalk.gray(` 2. Add scripts to ${path.join(skillDir, "scripts")}`));
|
|
597
|
+
console.log(chalk.gray(` 3. Run 'vellum skill validate --skill ${name}' to verify`));
|
|
598
|
+
|
|
599
|
+
return { success: true, path: skillDir, exitCode: EXIT_CODES.SUCCESS };
|
|
600
|
+
} catch (err) {
|
|
601
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
602
|
+
console.error(chalk.red(`\n${ICONS.error} Failed to create skill: ${message}`));
|
|
603
|
+
return { success: false, error: message, exitCode: EXIT_CODES.ERROR };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// =============================================================================
|
|
608
|
+
// Validate Command (T037)
|
|
609
|
+
// =============================================================================
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Validate a single skill
|
|
613
|
+
*/
|
|
614
|
+
async function validateSingleSkill(
|
|
615
|
+
skillPath: string,
|
|
616
|
+
parser: SkillParser,
|
|
617
|
+
strict: boolean
|
|
618
|
+
): Promise<SkillValidationResult> {
|
|
619
|
+
const errors: string[] = [];
|
|
620
|
+
const warnings: string[] = [];
|
|
621
|
+
const name = path.basename(skillPath);
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const manifestPath = path.join(skillPath, "SKILL.md");
|
|
625
|
+
|
|
626
|
+
// Check if SKILL.md exists
|
|
627
|
+
if (!(await fileExists(manifestPath))) {
|
|
628
|
+
errors.push("SKILL.md not found");
|
|
629
|
+
return { name, path: skillPath, valid: false, errors, warnings };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Parse the skill
|
|
633
|
+
const result = await parser.parseMetadata(manifestPath, "workspace");
|
|
634
|
+
|
|
635
|
+
if (!result) {
|
|
636
|
+
errors.push("Failed to parse SKILL.md");
|
|
637
|
+
return { name, path: skillPath, valid: false, errors, warnings };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Check required fields
|
|
641
|
+
if (!result.name || result.name.trim() === "") {
|
|
642
|
+
errors.push("Missing required field: name");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!result.description || result.description.trim() === "") {
|
|
646
|
+
warnings.push("Missing description");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!result.triggers || result.triggers.length === 0) {
|
|
650
|
+
warnings.push("No triggers defined - skill will only activate with 'always' trigger");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Validate triggers
|
|
654
|
+
for (const trigger of result.triggers || []) {
|
|
655
|
+
if (trigger.type !== "always" && !trigger.pattern) {
|
|
656
|
+
errors.push(`Trigger of type '${trigger.type}' must have a pattern`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// In strict mode, warnings become errors
|
|
661
|
+
if (strict && warnings.length > 0) {
|
|
662
|
+
errors.push(...warnings.map((w) => `[strict] ${w}`));
|
|
663
|
+
warnings.length = 0;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
name: result.name || name,
|
|
668
|
+
path: skillPath,
|
|
669
|
+
valid: errors.length === 0,
|
|
670
|
+
errors,
|
|
671
|
+
warnings,
|
|
672
|
+
};
|
|
673
|
+
} catch (err) {
|
|
674
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
675
|
+
errors.push(`Parse error: ${message}`);
|
|
676
|
+
return { name, path: skillPath, valid: false, errors, warnings };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Calculate validation summary from results */
|
|
681
|
+
function calculateValidationSummary(results: SkillValidationResult[]) {
|
|
682
|
+
return {
|
|
683
|
+
total: results.length,
|
|
684
|
+
valid: results.filter((r) => r.valid).length,
|
|
685
|
+
invalid: results.filter((r) => !r.valid).length,
|
|
686
|
+
warnings: results.reduce((acc, r) => acc + r.warnings.length, 0),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Format validation results as text lines */
|
|
691
|
+
function formatValidationResultLines(results: SkillValidationResult[]): string[] {
|
|
692
|
+
const lines: string[] = [];
|
|
693
|
+
for (const result of results) {
|
|
694
|
+
const icon = result.valid ? chalk.green(ICONS.success) : chalk.red(ICONS.error);
|
|
695
|
+
lines.push(`${icon} ${chalk.white(result.name)}`);
|
|
696
|
+
lines.push(chalk.gray(` ${result.path}`));
|
|
697
|
+
|
|
698
|
+
for (const err of result.errors) {
|
|
699
|
+
lines.push(chalk.red(` x ${err}`));
|
|
700
|
+
}
|
|
701
|
+
for (const warn of result.warnings) {
|
|
702
|
+
lines.push(chalk.yellow(` ${ICONS.warning} ${warn}`));
|
|
703
|
+
}
|
|
704
|
+
lines.push("");
|
|
705
|
+
}
|
|
706
|
+
return lines;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Format validation summary as text lines */
|
|
710
|
+
function formatValidationSummaryLines(
|
|
711
|
+
summary: ReturnType<typeof calculateValidationSummary>
|
|
712
|
+
): string[] {
|
|
713
|
+
const lines: string[] = [];
|
|
714
|
+
lines.push(chalk.gray("─".repeat(50)));
|
|
715
|
+
lines.push(
|
|
716
|
+
`${chalk.white("Total:")} ${summary.total} ` +
|
|
717
|
+
`${chalk.green("Valid:")} ${summary.valid} ` +
|
|
718
|
+
`${chalk.red("Invalid:")} ${summary.invalid} ` +
|
|
719
|
+
`${chalk.yellow("Warnings:")} ${summary.warnings}`
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
const allValid = summary.invalid === 0;
|
|
723
|
+
lines.push(
|
|
724
|
+
allValid
|
|
725
|
+
? chalk.green(`\n${ICONS.success} All skills are valid!`)
|
|
726
|
+
: chalk.red(`\n${ICONS.error} Some skills have errors.`)
|
|
727
|
+
);
|
|
728
|
+
return lines;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Execute skill validate command
|
|
733
|
+
*/
|
|
734
|
+
export async function handleSkillValidate(
|
|
735
|
+
options: SkillValidateOptions = {}
|
|
736
|
+
): Promise<CommandResult> {
|
|
737
|
+
try {
|
|
738
|
+
const workspacePath = process.cwd();
|
|
739
|
+
const parser = new SkillParser();
|
|
740
|
+
const discovery = new SkillDiscovery({ workspacePath });
|
|
741
|
+
const discovered = await discovery.discoverAll();
|
|
742
|
+
const results: SkillValidationResult[] = [];
|
|
743
|
+
|
|
744
|
+
if (options.skill) {
|
|
745
|
+
const skillLocation = discovered.deduplicated.find(
|
|
746
|
+
(loc: SkillLocation) => path.basename(loc.path) === options.skill
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
if (!skillLocation) {
|
|
750
|
+
if (options.json) {
|
|
751
|
+
const output: SkillValidateJson = {
|
|
752
|
+
success: false,
|
|
753
|
+
results: [],
|
|
754
|
+
summary: { total: 0, valid: 0, invalid: 1, warnings: 0 },
|
|
755
|
+
};
|
|
756
|
+
return error("RESOURCE_NOT_FOUND", JSON.stringify(output, null, 2));
|
|
757
|
+
}
|
|
758
|
+
return error("RESOURCE_NOT_FOUND", chalk.red(`Skill not found: ${options.skill}`));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
results.push(await validateSingleSkill(skillLocation.path, parser, options.strict ?? false));
|
|
762
|
+
} else {
|
|
763
|
+
for (const location of discovered.deduplicated) {
|
|
764
|
+
results.push(await validateSingleSkill(location.path, parser, options.strict ?? false));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const summary = calculateValidationSummary(results);
|
|
769
|
+
|
|
770
|
+
if (options.json) {
|
|
771
|
+
const output: SkillValidateJson = { success: summary.invalid === 0, results, summary };
|
|
772
|
+
return summary.invalid === 0
|
|
773
|
+
? success(JSON.stringify(output, null, 2))
|
|
774
|
+
: error("INVALID_ARGUMENT", JSON.stringify(output, null, 2));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const lines: string[] = [chalk.bold.cyan(`\n[Skill] Validation Results\n`)];
|
|
778
|
+
|
|
779
|
+
if (results.length === 0) {
|
|
780
|
+
lines.push(chalk.yellow("No skills found to validate."));
|
|
781
|
+
return success(lines.join("\n"));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
lines.push(...formatValidationResultLines(results));
|
|
785
|
+
lines.push(...formatValidationSummaryLines(summary));
|
|
786
|
+
|
|
787
|
+
return summary.invalid === 0
|
|
788
|
+
? success(lines.join("\n"))
|
|
789
|
+
: error("INVALID_ARGUMENT", lines.join("\n"));
|
|
790
|
+
} catch (err) {
|
|
791
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
792
|
+
return error("INTERNAL_ERROR", `Failed to validate skills: ${message}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// =============================================================================
|
|
797
|
+
// Migrate Command (T052)
|
|
798
|
+
// =============================================================================
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Supported migration sources
|
|
802
|
+
*/
|
|
803
|
+
export type MigrationSource = "claude" | "roo";
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Options for skill migrate command
|
|
807
|
+
*/
|
|
808
|
+
export interface SkillMigrateOptions {
|
|
809
|
+
/** Source format to migrate from */
|
|
810
|
+
from: MigrationSource;
|
|
811
|
+
/** Target location for migrated skills */
|
|
812
|
+
location?: SkillSource;
|
|
813
|
+
/** Output as JSON */
|
|
814
|
+
json?: boolean;
|
|
815
|
+
/** Dry run (don't actually write files) */
|
|
816
|
+
dryRun?: boolean;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* JSON output for skill migrate
|
|
821
|
+
*/
|
|
822
|
+
interface SkillMigrateJson {
|
|
823
|
+
success: boolean;
|
|
824
|
+
source: MigrationSource;
|
|
825
|
+
migrated: Array<{
|
|
826
|
+
originalPath: string;
|
|
827
|
+
targetPath: string;
|
|
828
|
+
name: string;
|
|
829
|
+
}>;
|
|
830
|
+
errors: Array<{
|
|
831
|
+
path: string;
|
|
832
|
+
error: string;
|
|
833
|
+
}>;
|
|
834
|
+
summary: {
|
|
835
|
+
total: number;
|
|
836
|
+
migrated: number;
|
|
837
|
+
failed: number;
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Get source paths based on migration source type
|
|
843
|
+
*/
|
|
844
|
+
function getMigrationSourcePaths(source: MigrationSource, workspacePath: string): string[] {
|
|
845
|
+
switch (source) {
|
|
846
|
+
case "claude":
|
|
847
|
+
return [
|
|
848
|
+
// Claude Code skill locations
|
|
849
|
+
path.join(workspacePath, ".github", "skills"),
|
|
850
|
+
path.join(workspacePath, ".claude", "skills"),
|
|
851
|
+
path.join(os.homedir(), ".claude", "skills"),
|
|
852
|
+
];
|
|
853
|
+
case "roo":
|
|
854
|
+
return [
|
|
855
|
+
// Roo Code skill locations
|
|
856
|
+
path.join(workspacePath, ".roo", "skills"),
|
|
857
|
+
path.join(os.homedir(), ".roo", "skills"),
|
|
858
|
+
];
|
|
859
|
+
default:
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Check if a SKILL.md needs migration (missing Vellum-specific fields)
|
|
866
|
+
*/
|
|
867
|
+
async function needsMigration(manifestPath: string): Promise<boolean> {
|
|
868
|
+
try {
|
|
869
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
870
|
+
// Check if it has Vellum-specific frontmatter fields
|
|
871
|
+
const hasVellumFields = content.includes("priority:") && content.includes("triggers:");
|
|
872
|
+
return !hasVellumFields;
|
|
873
|
+
} catch {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Parse old skill format and convert to Vellum format
|
|
880
|
+
*/
|
|
881
|
+
async function convertSkillFormat(manifestPath: string, source: MigrationSource): Promise<string> {
|
|
882
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
883
|
+
|
|
884
|
+
// Parse existing frontmatter
|
|
885
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
886
|
+
if (!frontmatterMatch) {
|
|
887
|
+
// No frontmatter, create new skill from content
|
|
888
|
+
const skillName = path.basename(path.dirname(manifestPath));
|
|
889
|
+
const firstLine = content.split("\n").find((l) => l.startsWith("# "));
|
|
890
|
+
const title = firstLine?.replace(/^#\s*/, "") ?? skillName;
|
|
891
|
+
|
|
892
|
+
return `---
|
|
893
|
+
name: "${skillName}"
|
|
894
|
+
description: "${title}"
|
|
895
|
+
version: "1.0.0"
|
|
896
|
+
priority: 50
|
|
897
|
+
tags:
|
|
898
|
+
- migrated
|
|
899
|
+
- ${source}
|
|
900
|
+
triggers:
|
|
901
|
+
- type: keyword
|
|
902
|
+
pattern: "${skillName}"
|
|
903
|
+
globs:
|
|
904
|
+
- "**/*"
|
|
905
|
+
---
|
|
906
|
+
|
|
907
|
+
${content}`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Parse existing YAML frontmatter - guaranteed to exist after the regex match
|
|
911
|
+
const frontmatter = frontmatterMatch[1] ?? "";
|
|
912
|
+
const body = content.slice(frontmatterMatch[0].length).trim();
|
|
913
|
+
|
|
914
|
+
// Extract existing fields
|
|
915
|
+
const nameMatch = frontmatter.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
916
|
+
|
|
917
|
+
const name = nameMatch?.[1] ?? path.basename(path.dirname(manifestPath));
|
|
918
|
+
|
|
919
|
+
// Check for existing triggers or globs
|
|
920
|
+
const hasExistingTriggers = frontmatter.includes("triggers:");
|
|
921
|
+
const hasExistingGlobs = frontmatter.includes("globs:");
|
|
922
|
+
const hasExistingPriority = frontmatter.includes("priority:");
|
|
923
|
+
|
|
924
|
+
// Build new frontmatter
|
|
925
|
+
let newFrontmatter = frontmatter.trim();
|
|
926
|
+
|
|
927
|
+
// Add priority if missing
|
|
928
|
+
if (!hasExistingPriority) {
|
|
929
|
+
newFrontmatter += `\npriority: 50`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Add tags
|
|
933
|
+
if (!frontmatter.includes("tags:")) {
|
|
934
|
+
newFrontmatter += `\ntags:\n - migrated\n - ${source}`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Add triggers if missing
|
|
938
|
+
if (!hasExistingTriggers) {
|
|
939
|
+
const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
940
|
+
newFrontmatter += `\ntriggers:\n - type: keyword\n pattern: "${safeName}"`;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Add globs if missing
|
|
944
|
+
if (!hasExistingGlobs) {
|
|
945
|
+
newFrontmatter += `\nglobs:\n - "**/*"`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return `---
|
|
949
|
+
${newFrontmatter}
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
${body}`;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Migration result for a single skill
|
|
957
|
+
*/
|
|
958
|
+
interface MigrationEntry {
|
|
959
|
+
originalPath: string;
|
|
960
|
+
targetPath: string;
|
|
961
|
+
name: string;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Migration error for a single skill
|
|
966
|
+
*/
|
|
967
|
+
interface MigrationError {
|
|
968
|
+
path: string;
|
|
969
|
+
error: string;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Migrate a single skill directory
|
|
974
|
+
*/
|
|
975
|
+
async function migrateSkillDirectory(
|
|
976
|
+
skillDir: string,
|
|
977
|
+
manifestPath: string,
|
|
978
|
+
targetBasePath: string,
|
|
979
|
+
source: MigrationSource,
|
|
980
|
+
dryRun: boolean
|
|
981
|
+
): Promise<{ entry?: MigrationEntry; error?: MigrationError }> {
|
|
982
|
+
const skillName = path.basename(skillDir);
|
|
983
|
+
const targetPath = path.join(targetBasePath, skillName);
|
|
984
|
+
const targetManifest = path.join(targetPath, "SKILL.md");
|
|
985
|
+
|
|
986
|
+
// Skip if target already exists (unless it's the same location)
|
|
987
|
+
if ((await fileExists(targetManifest)) && targetPath !== skillDir) {
|
|
988
|
+
return {
|
|
989
|
+
error: { path: manifestPath, error: `Target already exists: ${targetPath}` },
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
const needsConversion = await needsMigration(manifestPath);
|
|
995
|
+
const convertedContent = needsConversion
|
|
996
|
+
? await convertSkillFormat(manifestPath, source)
|
|
997
|
+
: await fs.readFile(manifestPath, "utf-8");
|
|
998
|
+
|
|
999
|
+
if (!dryRun) {
|
|
1000
|
+
await ensureDir(targetPath);
|
|
1001
|
+
await fs.writeFile(targetManifest, convertedContent, "utf-8");
|
|
1002
|
+
|
|
1003
|
+
// Copy subdirectories
|
|
1004
|
+
for (const subdir of ["scripts", "references", "assets"]) {
|
|
1005
|
+
const sourceSubdir = path.join(skillDir, subdir);
|
|
1006
|
+
const targetSubdir = path.join(targetPath, subdir);
|
|
1007
|
+
if (await fileExists(sourceSubdir)) {
|
|
1008
|
+
await fs.cp(sourceSubdir, targetSubdir, { recursive: true });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
entry: { originalPath: manifestPath, targetPath: targetManifest, name: skillName },
|
|
1015
|
+
};
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1018
|
+
return { error: { path: manifestPath, error: message } };
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Scan a source path for skills to migrate
|
|
1024
|
+
*/
|
|
1025
|
+
async function scanSourcePathForSkills(
|
|
1026
|
+
sourcePath: string,
|
|
1027
|
+
targetBasePath: string,
|
|
1028
|
+
source: MigrationSource,
|
|
1029
|
+
dryRun: boolean
|
|
1030
|
+
): Promise<{ migrated: MigrationEntry[]; errors: MigrationError[] }> {
|
|
1031
|
+
const migrated: MigrationEntry[] = [];
|
|
1032
|
+
const errors: MigrationError[] = [];
|
|
1033
|
+
|
|
1034
|
+
if (!(await fileExists(sourcePath))) {
|
|
1035
|
+
return { migrated, errors };
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
1040
|
+
|
|
1041
|
+
for (const entry of entries) {
|
|
1042
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const skillDir = path.join(sourcePath, entry.name);
|
|
1047
|
+
const manifestPath = path.join(skillDir, "SKILL.md");
|
|
1048
|
+
|
|
1049
|
+
if (!(await fileExists(manifestPath))) {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const result = await migrateSkillDirectory(
|
|
1054
|
+
skillDir,
|
|
1055
|
+
manifestPath,
|
|
1056
|
+
targetBasePath,
|
|
1057
|
+
source,
|
|
1058
|
+
dryRun
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
if (result.entry) {
|
|
1062
|
+
migrated.push(result.entry);
|
|
1063
|
+
}
|
|
1064
|
+
if (result.error) {
|
|
1065
|
+
errors.push(result.error);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1070
|
+
errors.push({ path: sourcePath, error: message });
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return { migrated, errors };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Format migration output as text
|
|
1078
|
+
*/
|
|
1079
|
+
function formatMigrationOutput(
|
|
1080
|
+
sourcePaths: string[],
|
|
1081
|
+
migrated: MigrationEntry[],
|
|
1082
|
+
errors: MigrationError[],
|
|
1083
|
+
targetBasePath: string,
|
|
1084
|
+
source: MigrationSource,
|
|
1085
|
+
dryRun: boolean
|
|
1086
|
+
): string {
|
|
1087
|
+
const lines: string[] = [];
|
|
1088
|
+
const modeLabel = dryRun ? " (dry run)" : "";
|
|
1089
|
+
lines.push(chalk.bold.cyan(`\n${ICONS.migrate} Skill Migration from ${source}${modeLabel}\n`));
|
|
1090
|
+
|
|
1091
|
+
if (migrated.length === 0 && errors.length === 0) {
|
|
1092
|
+
lines.push(chalk.yellow(`No ${source} skills found to migrate.`));
|
|
1093
|
+
lines.push(chalk.gray("\nSearched locations:"));
|
|
1094
|
+
for (const p of sourcePaths) {
|
|
1095
|
+
lines.push(chalk.gray(` • ${p}`));
|
|
1096
|
+
}
|
|
1097
|
+
return lines.join("\n");
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (migrated.length > 0) {
|
|
1101
|
+
lines.push(chalk.green(`${ICONS.success} Migrated Skills:`));
|
|
1102
|
+
for (const m of migrated) {
|
|
1103
|
+
lines.push(chalk.white(` ${ICONS.bullet} ${m.name}`));
|
|
1104
|
+
lines.push(chalk.gray(` ${m.originalPath}`));
|
|
1105
|
+
lines.push(chalk.gray(` -> ${m.targetPath}`));
|
|
1106
|
+
}
|
|
1107
|
+
lines.push("");
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (errors.length > 0) {
|
|
1111
|
+
lines.push(chalk.red(`${ICONS.error} Failed:`));
|
|
1112
|
+
for (const e of errors) {
|
|
1113
|
+
lines.push(chalk.red(` ${ICONS.bullet} ${e.path}`));
|
|
1114
|
+
lines.push(chalk.red(` ${e.error}`));
|
|
1115
|
+
}
|
|
1116
|
+
lines.push("");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const summary = {
|
|
1120
|
+
total: migrated.length + errors.length,
|
|
1121
|
+
migrated: migrated.length,
|
|
1122
|
+
failed: errors.length,
|
|
1123
|
+
};
|
|
1124
|
+
lines.push(chalk.gray("─".repeat(50)));
|
|
1125
|
+
lines.push(
|
|
1126
|
+
`${chalk.white("Total:")} ${summary.total} ` +
|
|
1127
|
+
`${chalk.green("Migrated:")} ${summary.migrated} ` +
|
|
1128
|
+
`${chalk.red("Failed:")} ${summary.failed}`
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
if (dryRun) {
|
|
1132
|
+
lines.push(chalk.yellow(`\n${ICONS.warning} Dry run - no files were modified.`));
|
|
1133
|
+
lines.push(chalk.gray("Remove --dry-run to perform actual migration."));
|
|
1134
|
+
} else if (summary.migrated > 0) {
|
|
1135
|
+
lines.push(
|
|
1136
|
+
chalk.green(`\n${ICONS.success} Successfully migrated ${summary.migrated} skill(s)!`)
|
|
1137
|
+
);
|
|
1138
|
+
lines.push(chalk.gray(`Target location: ${targetBasePath}`));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return lines.join("\n");
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Execute skill migrate command
|
|
1146
|
+
*/
|
|
1147
|
+
export async function handleSkillMigrate(options: SkillMigrateOptions): Promise<CommandResult> {
|
|
1148
|
+
try {
|
|
1149
|
+
const workspacePath = process.cwd();
|
|
1150
|
+
const sourcePaths = getMigrationSourcePaths(options.from, workspacePath);
|
|
1151
|
+
const targetLocation = options.location ?? "workspace";
|
|
1152
|
+
|
|
1153
|
+
if (targetLocation === "builtin") {
|
|
1154
|
+
return error("INVALID_ARGUMENT", "Cannot migrate skills to builtin location");
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const targetBasePath = getSkillSourcePath(targetLocation, workspacePath);
|
|
1158
|
+
const allMigrated: MigrationEntry[] = [];
|
|
1159
|
+
const allErrors: MigrationError[] = [];
|
|
1160
|
+
|
|
1161
|
+
// Scan all source paths
|
|
1162
|
+
for (const sourcePath of sourcePaths) {
|
|
1163
|
+
const { migrated, errors } = await scanSourcePathForSkills(
|
|
1164
|
+
sourcePath,
|
|
1165
|
+
targetBasePath,
|
|
1166
|
+
options.from,
|
|
1167
|
+
options.dryRun ?? false
|
|
1168
|
+
);
|
|
1169
|
+
allMigrated.push(...migrated);
|
|
1170
|
+
allErrors.push(...errors);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const summary = {
|
|
1174
|
+
total: allMigrated.length + allErrors.length,
|
|
1175
|
+
migrated: allMigrated.length,
|
|
1176
|
+
failed: allErrors.length,
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// JSON output
|
|
1180
|
+
if (options.json) {
|
|
1181
|
+
const output: SkillMigrateJson = {
|
|
1182
|
+
success: allErrors.length === 0,
|
|
1183
|
+
source: options.from,
|
|
1184
|
+
migrated: allMigrated,
|
|
1185
|
+
errors: allErrors,
|
|
1186
|
+
summary,
|
|
1187
|
+
};
|
|
1188
|
+
return summary.failed === 0
|
|
1189
|
+
? success(JSON.stringify(output, null, 2))
|
|
1190
|
+
: error("INTERNAL_ERROR", JSON.stringify(output, null, 2));
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Formatted output
|
|
1194
|
+
const outputText = formatMigrationOutput(
|
|
1195
|
+
sourcePaths,
|
|
1196
|
+
allMigrated,
|
|
1197
|
+
allErrors,
|
|
1198
|
+
targetBasePath,
|
|
1199
|
+
options.from,
|
|
1200
|
+
options.dryRun ?? false
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
return summary.failed === 0 ? success(outputText) : error("INTERNAL_ERROR", outputText);
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1206
|
+
return error("INTERNAL_ERROR", `Migration failed: ${message}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// =============================================================================
|
|
1211
|
+
// Export Command Definitions for Commander.js (T038)
|
|
1212
|
+
// =============================================================================
|
|
1213
|
+
|
|
1214
|
+
export {
|
|
1215
|
+
handleSkillList as executeSkillList,
|
|
1216
|
+
handleSkillShow as executeSkillShow,
|
|
1217
|
+
handleSkillCreate as executeSkillCreate,
|
|
1218
|
+
handleSkillValidate as executeSkillValidate,
|
|
1219
|
+
handleSkillMigrate as executeSkillMigrate,
|
|
1220
|
+
};
|