@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,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks and manages tutorial progress across lessons and steps.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/onboarding/tutorial/progress-tracker
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
INITIAL_LESSON_PROGRESS,
|
|
11
|
+
type Lesson,
|
|
12
|
+
type LessonProgress,
|
|
13
|
+
type TutorialProgress,
|
|
14
|
+
type TutorialStorage,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Progress Statistics
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detailed progress statistics
|
|
23
|
+
*/
|
|
24
|
+
export interface ProgressStats {
|
|
25
|
+
/** Total lessons in tutorial */
|
|
26
|
+
totalLessons: number;
|
|
27
|
+
/** Completed lessons count */
|
|
28
|
+
completedLessons: number;
|
|
29
|
+
/** Started but incomplete lessons */
|
|
30
|
+
inProgressLessons: number;
|
|
31
|
+
/** Not started lessons */
|
|
32
|
+
notStartedLessons: number;
|
|
33
|
+
/** Total steps across all lessons */
|
|
34
|
+
totalSteps: number;
|
|
35
|
+
/** Completed steps count */
|
|
36
|
+
completedSteps: number;
|
|
37
|
+
/** Completion percentage (0-100) */
|
|
38
|
+
completionPercent: number;
|
|
39
|
+
/** Total time spent in minutes */
|
|
40
|
+
totalTimeMinutes: number;
|
|
41
|
+
/** Estimated remaining time in minutes */
|
|
42
|
+
estimatedRemainingMinutes: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Progress Tracker Class
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tracks and manages tutorial progress
|
|
51
|
+
*/
|
|
52
|
+
export class ProgressTracker {
|
|
53
|
+
private storage: TutorialStorage;
|
|
54
|
+
private lessons: readonly Lesson[];
|
|
55
|
+
private cachedProgress: TutorialProgress | null = null;
|
|
56
|
+
|
|
57
|
+
constructor(storage: TutorialStorage, lessons: readonly Lesson[]) {
|
|
58
|
+
this.storage = storage;
|
|
59
|
+
this.lessons = lessons;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ===========================================================================
|
|
63
|
+
// Progress Loading
|
|
64
|
+
// ===========================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load progress from storage
|
|
68
|
+
*/
|
|
69
|
+
async loadProgress(): Promise<TutorialProgress> {
|
|
70
|
+
this.cachedProgress = await this.storage.loadProgress();
|
|
71
|
+
return this.cachedProgress;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get cached progress or load if not cached
|
|
76
|
+
*/
|
|
77
|
+
async getProgress(): Promise<TutorialProgress> {
|
|
78
|
+
if (!this.cachedProgress) {
|
|
79
|
+
return this.loadProgress();
|
|
80
|
+
}
|
|
81
|
+
return this.cachedProgress;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ===========================================================================
|
|
85
|
+
// Lesson Progress
|
|
86
|
+
// ===========================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Start a lesson
|
|
90
|
+
*/
|
|
91
|
+
async startLesson(lessonId: string): Promise<LessonProgress> {
|
|
92
|
+
const progress = await this.getProgress();
|
|
93
|
+
const existing = progress.lessons[lessonId];
|
|
94
|
+
|
|
95
|
+
// Already started
|
|
96
|
+
if (existing?.started) {
|
|
97
|
+
return existing;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const now = new Date().toISOString();
|
|
101
|
+
const lessonProgress: LessonProgress = {
|
|
102
|
+
...INITIAL_LESSON_PROGRESS,
|
|
103
|
+
lessonId,
|
|
104
|
+
started: true,
|
|
105
|
+
startedAt: now,
|
|
106
|
+
currentStepIndex: 0,
|
|
107
|
+
completedSteps: [],
|
|
108
|
+
totalTimeSpent: 0,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
await this.storage.updateLessonProgress(lessonId, lessonProgress);
|
|
112
|
+
|
|
113
|
+
// Update cache
|
|
114
|
+
progress.lessons[lessonId] = lessonProgress;
|
|
115
|
+
progress.lastActiveLessonId = lessonId;
|
|
116
|
+
progress.lastActivityAt = now;
|
|
117
|
+
|
|
118
|
+
if (!progress.startedAt) {
|
|
119
|
+
progress.startedAt = now;
|
|
120
|
+
await this.storage.saveProgress(progress);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.cachedProgress = progress;
|
|
124
|
+
return lessonProgress;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Complete a step in a lesson
|
|
129
|
+
*/
|
|
130
|
+
async completeStep(lessonId: string, stepId: string, timeSpent?: number): Promise<void> {
|
|
131
|
+
const progress = await this.getProgress();
|
|
132
|
+
const lessonProgress = progress.lessons[lessonId];
|
|
133
|
+
|
|
134
|
+
if (!lessonProgress) {
|
|
135
|
+
throw new Error(`Lesson ${lessonId} not started`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Already completed
|
|
139
|
+
if (lessonProgress.completedSteps.includes(stepId)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Update lesson progress
|
|
144
|
+
const updatedSteps = [...lessonProgress.completedSteps, stepId];
|
|
145
|
+
const updatedTime = lessonProgress.totalTimeSpent + (timeSpent ?? 0);
|
|
146
|
+
|
|
147
|
+
await this.storage.updateLessonProgress(lessonId, {
|
|
148
|
+
completedSteps: updatedSteps,
|
|
149
|
+
currentStepIndex: updatedSteps.length,
|
|
150
|
+
totalTimeSpent: updatedTime,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Update cache
|
|
154
|
+
lessonProgress.completedSteps = updatedSteps;
|
|
155
|
+
lessonProgress.currentStepIndex = updatedSteps.length;
|
|
156
|
+
lessonProgress.totalTimeSpent = updatedTime;
|
|
157
|
+
progress.stepsCompleted++;
|
|
158
|
+
progress.lastActivityAt = new Date().toISOString();
|
|
159
|
+
|
|
160
|
+
this.cachedProgress = progress;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Complete a lesson
|
|
165
|
+
*/
|
|
166
|
+
async completeLesson(lessonId: string): Promise<void> {
|
|
167
|
+
const progress = await this.getProgress();
|
|
168
|
+
const lessonProgress = progress.lessons[lessonId];
|
|
169
|
+
|
|
170
|
+
if (!lessonProgress) {
|
|
171
|
+
throw new Error(`Lesson ${lessonId} not started`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Already completed
|
|
175
|
+
if (lessonProgress.completed) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const now = new Date().toISOString();
|
|
180
|
+
|
|
181
|
+
await this.storage.updateLessonProgress(lessonId, {
|
|
182
|
+
completed: true,
|
|
183
|
+
completedAt: now,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Update cache
|
|
187
|
+
lessonProgress.completed = true;
|
|
188
|
+
lessonProgress.completedAt = now;
|
|
189
|
+
progress.lessonsCompleted++;
|
|
190
|
+
progress.lastActivityAt = now;
|
|
191
|
+
|
|
192
|
+
await this.storage.saveProgress(progress);
|
|
193
|
+
this.cachedProgress = progress;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get progress for a specific lesson
|
|
198
|
+
*/
|
|
199
|
+
async getLessonProgress(lessonId: string): Promise<LessonProgress | undefined> {
|
|
200
|
+
const progress = await this.getProgress();
|
|
201
|
+
return progress.lessons[lessonId];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if lesson is completed
|
|
206
|
+
*/
|
|
207
|
+
async isLessonCompleted(lessonId: string): Promise<boolean> {
|
|
208
|
+
const lessonProgress = await this.getLessonProgress(lessonId);
|
|
209
|
+
return lessonProgress?.completed ?? false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get current step in a lesson
|
|
214
|
+
*/
|
|
215
|
+
async getCurrentStep(lessonId: string): Promise<number> {
|
|
216
|
+
const lessonProgress = await this.getLessonProgress(lessonId);
|
|
217
|
+
return lessonProgress?.currentStepIndex ?? 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ===========================================================================
|
|
221
|
+
// Statistics
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get detailed progress statistics
|
|
226
|
+
*/
|
|
227
|
+
async getStats(): Promise<ProgressStats> {
|
|
228
|
+
const progress = await this.getProgress();
|
|
229
|
+
|
|
230
|
+
let completedLessons = 0;
|
|
231
|
+
let inProgressLessons = 0;
|
|
232
|
+
let notStartedLessons = 0;
|
|
233
|
+
let totalSteps = 0;
|
|
234
|
+
let completedSteps = 0;
|
|
235
|
+
let totalTimeSeconds = 0;
|
|
236
|
+
let estimatedRemainingSeconds = 0;
|
|
237
|
+
|
|
238
|
+
for (const lesson of this.lessons) {
|
|
239
|
+
const lessonProgress = progress.lessons[lesson.id];
|
|
240
|
+
totalSteps += lesson.steps.length;
|
|
241
|
+
|
|
242
|
+
if (lessonProgress?.completed) {
|
|
243
|
+
completedLessons++;
|
|
244
|
+
completedSteps += lesson.steps.length;
|
|
245
|
+
totalTimeSeconds += lessonProgress.totalTimeSpent;
|
|
246
|
+
} else if (lessonProgress?.started) {
|
|
247
|
+
inProgressLessons++;
|
|
248
|
+
completedSteps += lessonProgress.completedSteps.length;
|
|
249
|
+
totalTimeSeconds += lessonProgress.totalTimeSpent;
|
|
250
|
+
|
|
251
|
+
// Estimate remaining for in-progress lesson
|
|
252
|
+
const remainingSteps = lesson.steps.length - lessonProgress.completedSteps.length;
|
|
253
|
+
const avgStepTime = (lesson.estimatedMinutes * 60) / lesson.steps.length;
|
|
254
|
+
estimatedRemainingSeconds += remainingSteps * avgStepTime;
|
|
255
|
+
} else {
|
|
256
|
+
notStartedLessons++;
|
|
257
|
+
estimatedRemainingSeconds += lesson.estimatedMinutes * 60;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const totalLessons = this.lessons.length;
|
|
262
|
+
const completionPercent = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
totalLessons,
|
|
266
|
+
completedLessons,
|
|
267
|
+
inProgressLessons,
|
|
268
|
+
notStartedLessons,
|
|
269
|
+
totalSteps,
|
|
270
|
+
completedSteps,
|
|
271
|
+
completionPercent,
|
|
272
|
+
totalTimeMinutes: Math.round(totalTimeSeconds / 60),
|
|
273
|
+
estimatedRemainingMinutes: Math.round(estimatedRemainingSeconds / 60),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get completion percentage for a lesson
|
|
279
|
+
*/
|
|
280
|
+
async getLessonCompletionPercent(lessonId: string): Promise<number> {
|
|
281
|
+
const lesson = this.lessons.find((l) => l.id === lessonId);
|
|
282
|
+
if (!lesson) return 0;
|
|
283
|
+
|
|
284
|
+
const lessonProgress = await this.getLessonProgress(lessonId);
|
|
285
|
+
if (!lessonProgress) return 0;
|
|
286
|
+
|
|
287
|
+
const totalSteps = lesson.steps.length;
|
|
288
|
+
const completedSteps = lessonProgress.completedSteps.length;
|
|
289
|
+
|
|
290
|
+
return totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if all tutorials are complete
|
|
295
|
+
*/
|
|
296
|
+
async isAllComplete(): Promise<boolean> {
|
|
297
|
+
const progress = await this.getProgress();
|
|
298
|
+
return this.lessons.every((lesson) => progress.lessons[lesson.id]?.completed);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get completed lesson IDs
|
|
303
|
+
*/
|
|
304
|
+
async getCompletedLessonIds(): Promise<string[]> {
|
|
305
|
+
const progress = await this.getProgress();
|
|
306
|
+
return Object.entries(progress.lessons)
|
|
307
|
+
.filter(([_, p]) => (p as { completed: boolean }).completed)
|
|
308
|
+
.map(([id]) => id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ===========================================================================
|
|
312
|
+
// Reset
|
|
313
|
+
// ===========================================================================
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Reset all progress
|
|
317
|
+
*/
|
|
318
|
+
async resetAll(): Promise<void> {
|
|
319
|
+
await this.storage.resetProgress();
|
|
320
|
+
this.cachedProgress = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reset a single lesson
|
|
325
|
+
*/
|
|
326
|
+
async resetLesson(lessonId: string): Promise<void> {
|
|
327
|
+
await this.storage.updateLessonProgress(lessonId, {
|
|
328
|
+
...INITIAL_LESSON_PROGRESS,
|
|
329
|
+
lessonId,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (this.cachedProgress) {
|
|
333
|
+
delete this.cachedProgress.lessons[lessonId];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// =============================================================================
|
|
339
|
+
// Factory
|
|
340
|
+
// =============================================================================
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create a progress tracker
|
|
344
|
+
*/
|
|
345
|
+
export function createProgressTracker(
|
|
346
|
+
storage: TutorialStorage,
|
|
347
|
+
lessons: readonly Lesson[]
|
|
348
|
+
): ProgressTracker {
|
|
349
|
+
return new ProgressTracker(storage, lessons);
|
|
350
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tutorial Storage
|
|
3
|
+
*
|
|
4
|
+
* Handles persistence of tutorial progress to disk.
|
|
5
|
+
*
|
|
6
|
+
* @module cli/onboarding/tutorial/storage
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as fsPromises from "node:fs/promises";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
INITIAL_LESSON_PROGRESS,
|
|
16
|
+
INITIAL_TUTORIAL_PROGRESS,
|
|
17
|
+
type LessonProgress,
|
|
18
|
+
type TutorialProgress,
|
|
19
|
+
TutorialProgressSchema,
|
|
20
|
+
type TutorialStorage,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Paths
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get Vellum config directory
|
|
29
|
+
*/
|
|
30
|
+
function getVellumDir(): string {
|
|
31
|
+
const home = os.homedir();
|
|
32
|
+
return path.join(home, ".vellum");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get tutorial progress file path
|
|
37
|
+
*/
|
|
38
|
+
function getTutorialProgressPath(): string {
|
|
39
|
+
return path.join(getVellumDir(), "tutorial-progress.json");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// File Storage Implementation
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* File-based tutorial storage
|
|
48
|
+
*/
|
|
49
|
+
export class FileTutorialStorage implements TutorialStorage {
|
|
50
|
+
private progressPath: string;
|
|
51
|
+
private cachedProgress: TutorialProgress | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(customPath?: string) {
|
|
54
|
+
this.progressPath = customPath ?? getTutorialProgressPath();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ensure the storage directory exists
|
|
59
|
+
*/
|
|
60
|
+
private async ensureDir(): Promise<void> {
|
|
61
|
+
const dir = path.dirname(this.progressPath);
|
|
62
|
+
if (!fs.existsSync(dir)) {
|
|
63
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load tutorial progress from file
|
|
69
|
+
*/
|
|
70
|
+
async loadProgress(): Promise<TutorialProgress> {
|
|
71
|
+
// Return cached if available
|
|
72
|
+
if (this.cachedProgress) {
|
|
73
|
+
return this.cachedProgress;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.existsSync(this.progressPath)) {
|
|
78
|
+
return { ...INITIAL_TUTORIAL_PROGRESS };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const content = await fsPromises.readFile(this.progressPath, "utf-8");
|
|
82
|
+
const data = JSON.parse(content) as unknown;
|
|
83
|
+
const parsed = TutorialProgressSchema.safeParse(data);
|
|
84
|
+
|
|
85
|
+
if (parsed.success) {
|
|
86
|
+
this.cachedProgress = parsed.data;
|
|
87
|
+
return parsed.data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Invalid data, return initial state
|
|
91
|
+
console.warn("[TutorialStorage] Invalid progress data, resetting");
|
|
92
|
+
return { ...INITIAL_TUTORIAL_PROGRESS };
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// File doesn't exist or parse error
|
|
95
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
96
|
+
console.warn("[TutorialStorage] Failed to load progress:", error);
|
|
97
|
+
}
|
|
98
|
+
return { ...INITIAL_TUTORIAL_PROGRESS };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Save tutorial progress to file
|
|
104
|
+
*/
|
|
105
|
+
async saveProgress(progress: TutorialProgress): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
await this.ensureDir();
|
|
108
|
+
|
|
109
|
+
// Validate before saving
|
|
110
|
+
const parsed = TutorialProgressSchema.safeParse(progress);
|
|
111
|
+
if (!parsed.success) {
|
|
112
|
+
throw new Error(`Invalid progress data: ${parsed.error.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Update cache
|
|
116
|
+
this.cachedProgress = parsed.data;
|
|
117
|
+
|
|
118
|
+
// Write to file
|
|
119
|
+
const content = JSON.stringify(parsed.data, null, 2);
|
|
120
|
+
await fsPromises.writeFile(this.progressPath, content, "utf-8");
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("[TutorialStorage] Failed to save progress:", error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reset all progress
|
|
129
|
+
*/
|
|
130
|
+
async resetProgress(): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
this.cachedProgress = null;
|
|
133
|
+
|
|
134
|
+
if (fs.existsSync(this.progressPath)) {
|
|
135
|
+
await fsPromises.unlink(this.progressPath);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error("[TutorialStorage] Failed to reset progress:", error);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get progress for a specific lesson
|
|
145
|
+
*/
|
|
146
|
+
async getLessonProgress(lessonId: string): Promise<LessonProgress | undefined> {
|
|
147
|
+
const progress = await this.loadProgress();
|
|
148
|
+
return progress.lessons[lessonId];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Update progress for a specific lesson
|
|
153
|
+
*/
|
|
154
|
+
async updateLessonProgress(lessonId: string, updates: Partial<LessonProgress>): Promise<void> {
|
|
155
|
+
const progress = await this.loadProgress();
|
|
156
|
+
|
|
157
|
+
// Get or create lesson progress
|
|
158
|
+
const existing = progress.lessons[lessonId] ?? {
|
|
159
|
+
...INITIAL_LESSON_PROGRESS,
|
|
160
|
+
lessonId,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Merge updates
|
|
164
|
+
progress.lessons[lessonId] = {
|
|
165
|
+
...existing,
|
|
166
|
+
...updates,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Update last activity
|
|
170
|
+
progress.lastActivityAt = new Date().toISOString();
|
|
171
|
+
progress.lastActiveLessonId = lessonId;
|
|
172
|
+
|
|
173
|
+
await this.saveProgress(progress);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Clear the cache (useful for testing)
|
|
178
|
+
*/
|
|
179
|
+
clearCache(): void {
|
|
180
|
+
this.cachedProgress = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// In-Memory Storage (for testing)
|
|
186
|
+
// =============================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* In-memory tutorial storage for testing
|
|
190
|
+
*/
|
|
191
|
+
export class MemoryTutorialStorage implements TutorialStorage {
|
|
192
|
+
private progress: TutorialProgress = { ...INITIAL_TUTORIAL_PROGRESS };
|
|
193
|
+
|
|
194
|
+
async loadProgress(): Promise<TutorialProgress> {
|
|
195
|
+
return { ...this.progress };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async saveProgress(progress: TutorialProgress): Promise<void> {
|
|
199
|
+
this.progress = { ...progress };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async resetProgress(): Promise<void> {
|
|
203
|
+
this.progress = { ...INITIAL_TUTORIAL_PROGRESS };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async getLessonProgress(lessonId: string): Promise<LessonProgress | undefined> {
|
|
207
|
+
return this.progress.lessons[lessonId];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async updateLessonProgress(lessonId: string, updates: Partial<LessonProgress>): Promise<void> {
|
|
211
|
+
const existing = this.progress.lessons[lessonId] ?? {
|
|
212
|
+
...INITIAL_LESSON_PROGRESS,
|
|
213
|
+
lessonId,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
this.progress.lessons[lessonId] = {
|
|
217
|
+
...existing,
|
|
218
|
+
...updates,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
this.progress.lastActivityAt = new Date().toISOString();
|
|
222
|
+
this.progress.lastActiveLessonId = lessonId;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get raw progress (for testing)
|
|
227
|
+
*/
|
|
228
|
+
getRawProgress(): TutorialProgress {
|
|
229
|
+
return this.progress;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Set raw progress (for testing)
|
|
234
|
+
*/
|
|
235
|
+
setRawProgress(progress: TutorialProgress): void {
|
|
236
|
+
this.progress = progress;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// =============================================================================
|
|
241
|
+
// Factory
|
|
242
|
+
// =============================================================================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create a tutorial storage instance
|
|
246
|
+
*/
|
|
247
|
+
export function createTutorialStorage(customPath?: string): TutorialStorage {
|
|
248
|
+
return new FileTutorialStorage(customPath);
|
|
249
|
+
}
|