diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dc9582ba5d..bdddcf27e6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,7 +4,8 @@ /acceptance/pipelines/ @jefferycheng1 @kanterov @lennartkats-db /cmd/pipelines/ @jefferycheng1 @kanterov @lennartkats-db /cmd/labs/ @alexott @nfx -/experimental/aitools/ @databricks/eng-app-devex @lennartkats-db /cmd/workspace/apps/ @databricks/eng-app-devex /libs/apps/ @databricks/eng-app-devex /acceptance/apps/ @databricks/eng-app-devex +/experimental/aitools/ @databricks/eng-app-devex @pietern @lennartkats-db +/experimental/apps-mcp/ @databricks/eng-app-devex @pietern @lennartkats-db diff --git a/cmd/experimental/experimental.go b/cmd/experimental/experimental.go index 49e7c2578a..79712b5679 100644 --- a/cmd/experimental/experimental.go +++ b/cmd/experimental/experimental.go @@ -2,6 +2,7 @@ package experimental import ( "github.com/databricks/cli/experimental/aitools" + mcp "github.com/databricks/cli/experimental/apps-mcp/cmd" "github.com/spf13/cobra" ) @@ -21,6 +22,7 @@ development. They may change or be removed in future versions without notice.`, } cmd.AddCommand(aitools.New()) + cmd.AddCommand(mcp.NewMcpCmd()) return cmd } diff --git a/experimental/apps-mcp/README.md b/experimental/apps-mcp/README.md new file mode 100644 index 0000000000..a15d8bb496 --- /dev/null +++ b/experimental/apps-mcp/README.md @@ -0,0 +1,347 @@ +# Databricks MCP Server + +A Model Context Protocol (MCP) server for generating production-ready Databricks applications with testing, +linting and deployment setup from a single prompt. This agent relies heavily on scaffolding and +extensive validation to ensure high-quality outputs. + +## TL;DR + +**Primary Goal:** Create and deploy production-ready Databricks applications from a single natural language prompt. This MCP server combines scaffolding, validation, and deployment into a seamless workflow that goes from idea to running application. + +**How it works:** +1. **Explore your data** - Query Databricks catalogs, schemas, and tables to understand your data +2. **Generate the app** - Scaffold a full-stack TypeScript application (tRPC + React) with proper structure +3. **Customize with AI** - Use workspace tools to read, write, and edit files naturally through conversation +4. **Validate rigorously** - Run builds, type checks, and tests to ensure quality +5. **Deploy confidently** - Push validated apps directly to Databricks Apps platform + +**Why use it:** +- **Speed**: Go from concept to deployed Databricks app in minutes, not hours or days +- **Quality**: Extensive validation ensures your app builds, passes tests, and is production-ready +- **Simplicity**: One natural language conversation handles the entire workflow + +Perfect for data engineers and developers who want to build Databricks apps without the manual overhead of project setup, configuration, testing infrastructure, and deployment pipelines. + +--- + +## Getting Started + +### Quick Setup + +1. **Set up Databricks credentials** (required for Databricks tools): + ```bash + export DATABRICKS_HOST="https://your-workspace.databricks.com" + export DATABRICKS_TOKEN="dapi..." + export DATABRICKS_WAREHOUSE_ID="your-warehouse-id" + ``` + +2. **Configure your MCP client** (e.g., Claude Code): + + Add to your MCP config file (e.g., `~/.claude.json`): + ```json + { + "mcpServers": { + "databricks": { + "command": "databricks", + "args": ["experimental", "apps-mcp"], + "env": { + "DATABRICKS_HOST": "https://your-workspace.databricks.com", + "DATABRICKS_TOKEN": "dapi...", + "DATABRICKS_WAREHOUSE_ID": "your-warehouse-id" + } + } + } + } + ``` + +3. **Create your first Databricks app:** + + Restart your MCP client and try: + ``` + Create a Databricks app that shows sales data from main.sales.transactions + with a chart showing revenue by region. Deploy it as "sales-dashboard". + ``` + + The AI will: + - Explore your Databricks tables + - Generate a full-stack application + - Customize it based on your requirements + - Validate it passes all tests + - Deploy it to Databricks Apps + +--- + +## Features + +All features are designed to support the end-to-end workflow of creating production-ready Databricks applications: + +### 1. Data Exploration (Foundation) + +Understand your Databricks data before building: + +- **`databricks_list_catalogs`** - Discover available data catalogs +- **`databricks_list_schemas`** - Browse schemas in a catalog +- **`databricks_list_tables`** - Find tables in a schema +- **`databricks_describe_table`** - Get table details, columns, and sample data +- **`databricks_execute_query`** - Test queries and preview data + +*These tools help the AI understand your data structure so it can generate relevant application code.* + +### 2. Application Generation (Core) + +Create the application structure: + +- **`scaffold_data_app`** - Generate a full-stack TypeScript application + - Modern stack: Node.js, TypeScript, React, tRPC + - Pre-configured build system, linting, and testing + - Production-ready project structure + - Databricks SDK integration + +*This is the foundation of your application - a working, tested template ready for customization.* + +### 3. Validation (Quality Assurance) + +Ensure production-readiness before deployment: + +- **`validate_data_app`** - Comprehensive validation + - Build verification (npm build) + - Type checking (TypeScript compiler) + - Test execution (full test suite) + +*This step guarantees your application is tested and ready for production before deployment.* + +### 4. Deployment (Production Release) + +Deploy validated applications to Databricks (enable with `--allow-deployment`): + +- **`deploy_databricks_app`** - Push to Databricks Apps platform + - Automatic deployment configuration + - Environment management + - Production-grade setup + +*The final step: your validated application running on Databricks.* + +--- + +## Example Usage + +Here are example conversations showing the end-to-end workflow for creating Databricks applications: + +### Complete Workflow: Analytics Dashboard + +This example shows how to go from data exploration to deployed application: + +**User:** +``` +I want to create a Databricks app that visualizes customer purchases. The data is +in the main.sales catalog. Show me what tables are available and create a dashboard +with charts for total revenue by region and top products. Deploy it as "sales-insights". +``` + +**What happens:** +1. **Data Discovery** - AI lists schemas and tables in main.sales +2. **Data Inspection** - AI describes the purchases table structure +3. **App Generation** - AI scaffolds a TypeScript application +4. **Customization** - AI adds visualization components and queries +5. **Validation** - AI runs build, type check, and tests in container +6. **Deployment** - AI deploys to Databricks Apps as "sales-insights" + +**Result:** A production-ready Databricks app running in minutes with proper testing. + +--- + +### Quick Examples for Specific Use Cases + +#### Data App from Scratch + +``` +Create a Databricks app in ~/projects/user-analytics that shows daily active users +from main.analytics.events. Include a line chart and data table. +``` + +#### Real-Time Monitoring Dashboard + +``` +Build a monitoring dashboard for the main.logs.system_metrics table. Show CPU, +memory, and disk usage over time. Add alerts for values above thresholds. +``` + +#### Report Generator + +``` +Create an app that generates weekly reports from main.sales.transactions. +Include revenue trends, top customers, and product performance. Add export to CSV. +``` + +#### Data Quality Dashboard + +``` +Build a data quality dashboard for main.warehouse.inventory. Check for nulls, +duplicates, and out-of-range values. Show data freshness metrics. +``` + +--- + +### Working with Existing Applications + +Once an app is scaffolded, you can continue development through conversation: + +``` +Add a filter to show only transactions from the last 30 days +``` + +``` +Update the chart to use a bar chart instead of line chart +``` + +``` +Add a new API endpoint to fetch customer details +``` + +``` +Run the tests and fix any failures +``` + +``` +Add error handling for failed database queries +``` + +--- + +### Iterative Development Workflow + +**Initial Request:** +``` +Create a simple dashboard for main.sales.orders +``` + +**Refinement:** +``` +Add a date range picker to filter orders +``` + +**Enhancement:** +``` +Include a summary card showing total orders and revenue +``` + +**Quality Check:** +``` +Validate the app and show me any test failures +``` + +**Production:** +``` +Deploy the app to Databricks as "orders-dashboard" +``` + +--- + +## Why This Approach Works + +### Traditional Development vs. Databricks MCP + +| Traditional Approach | With Databricks MCP | +|---------------------|-------------| +| Manual project setup (hours) | Instant scaffolding (seconds) | +| Configure build tools manually | Pre-configured and tested | +| Set up testing infrastructure | Built-in test suite | +| Manual code changes and debugging | AI-powered development with validation | +| Local testing only | Containerized validation (reproducible) | +| Manual deployment setup | Automated deployment to Databricks | +| **Time to production: days/weeks** | **Time to production: minutes** | + +### Key Advantages + +**1. Scaffolding + Validation = Quality** +- Start with a working, tested template +- Every change is validated before deployment +- No broken builds reach production + +**2. Natural Language = Productivity** +- Describe what you want, not how to build it +- AI handles implementation details +- Focus on requirements, not configuration + +**3. End-to-End Workflow = Simplicity** +- Single tool for entire lifecycle +- No context switching between tools +- Seamless progression from idea to deployment + +### What Makes It Production-Ready + +The Databricks MCP server doesn't just generate code—it ensures quality: + +- ✅ **TypeScript** - Type safety catches errors early +- ✅ **Build verification** - Ensures code compiles +- ✅ **Test suite** - Validates functionality +- ✅ **Linting** - Enforces code quality +- ✅ **Databricks integration** - Native SDK usage + +--- + +## Reference + +### CLI Commands + +```bash +# Start MCP server (default mode) +databricks experimental apps-mcp --warehouse-id + +# Enable workspace tools +databricks experimental apps-mcp --warehouse-id --with-workspace-tools + +# Enable deployment +databricks experimental apps-mcp --warehouse-id --allow-deployment +``` + +### CLI Flags + +| Flag | Description | Default | +|------|-------------|---------| +| `--warehouse-id` | Databricks SQL Warehouse ID (required) | - | +| `--with-workspace-tools` | Enable workspace file operations | `false` | +| `--allow-deployment` | Enable deployment operations | `false` | +| `--help` | Show help | - | + +### Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `DATABRICKS_HOST` | Databricks workspace URL | `https://your-workspace.databricks.com` | +| `DATABRICKS_TOKEN` | Databricks personal access token | `dapi...` | +| `WAREHOUSE_ID` | Databricks SQL warehouse ID (preferred) | `abc123def456` | +| `DATABRICKS_WAREHOUSE_ID` | Alternative name for warehouse ID | `abc123def456` | +| `ALLOW_DEPLOYMENT` | Enable deployment operations | `true` or `false` | +| `WITH_WORKSPACE_TOOLS` | Enable workspace tools | `true` or `false` | + +### Authentication + +The MCP server uses standard Databricks CLI authentication methods: + +1. **Environment variables** (as shown in the config above) +2. **Databricks CLI profiles** - Use `--profile` flag or `DATABRICKS_PROFILE` env var +3. **Default profile** - Uses `~/.databrickscfg` default profile if available + +For more details, see the [Databricks authentication documentation](https://docs.databricks.com/en/dev-tools/cli/authentication.html). + +### Requirements + +- **Databricks CLI** (this package) +- **Databricks workspace** with a SQL warehouse +- **MCP-compatible client** (Claude Desktop, Continue, etc.) + +--- + +## License + +See the main repository license. + +## Contributing + +Contributions welcome! Please see the main repository for development guidelines. + +## Support + +- **Issues**: https://github.com/databricks/cli/issues +- **Documentation**: https://docs.databricks.com/dev-tools/cli/databricks-cli.html diff --git a/experimental/apps-mcp/cmd/apps_mcp.go b/experimental/apps-mcp/cmd/apps_mcp.go new file mode 100644 index 0000000000..fcaee1f588 --- /dev/null +++ b/experimental/apps-mcp/cmd/apps_mcp.go @@ -0,0 +1,76 @@ +package mcp + +import ( + "github.com/databricks/cli/cmd/root" + mcplib "github.com/databricks/cli/experimental/apps-mcp/lib" + "github.com/databricks/cli/experimental/apps-mcp/lib/server" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/log" + "github.com/spf13/cobra" +) + +func NewMcpCmd() *cobra.Command { + var warehouseID string + var allowDeployment bool + var withWorkspaceTools bool + + cmd := &cobra.Command{ + Use: "apps-mcp", + Hidden: true, + Short: "Model Context Protocol server for AI agents", + Long: `Start and manage an MCP server that provides AI agents with tools to interact with Databricks. + +The MCP server exposes the following capabilities: +- Databricks integration (query catalogs, schemas, tables, execute SQL) +- Project scaffolding (generate full-stack TypeScript applications) +- Sandboxed execution (isolated file/command execution) + +The server communicates via stdio using the Model Context Protocol.`, + Example: ` # Start MCP server with required warehouse + databricks experimental apps-mcp --warehouse-id abc123 + + # Start with workspace tools enabled + databricks experimental apps-mcp --warehouse-id abc123 --with-workspace-tools + + # Start with deployment tools enabled + databricks experimental apps-mcp --warehouse-id abc123 --allow-deployment`, + PreRunE: root.MustWorkspaceClient, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + w := cmdctx.WorkspaceClient(ctx) + + // Build MCP config from flags + cfg := &mcplib.Config{ + AllowDeployment: allowDeployment, + WithWorkspaceTools: withWorkspaceTools, + WarehouseID: warehouseID, + DatabricksHost: w.Config.Host, + IoConfig: &mcplib.IoConfig{ + Validation: &mcplib.ValidationConfig{}, + }, + } + + log.Infof(ctx, "Starting MCP server") + + // Create and start server with workspace client in context + srv := server.NewServer(ctx, cfg) + + // Register tools + if err := srv.RegisterTools(ctx); err != nil { + log.Errorf(ctx, "Failed to register tools: %s", err) + return err + } + + // Run server + return srv.Run(ctx) + }, + } + + // Define flags + cmd.Flags().StringVar(&warehouseID, "warehouse-id", "", "Databricks SQL Warehouse ID") + cmd.Flags().BoolVar(&allowDeployment, "allow-deployment", false, "Enable deployment tools") + cmd.Flags().BoolVar(&withWorkspaceTools, "with-workspace-tools", false, "Enable workspace tools (file operations, bash, grep, glob)") + + return cmd +} diff --git a/experimental/apps-mcp/lib/config.go b/experimental/apps-mcp/lib/config.go new file mode 100644 index 0000000000..afeddd655e --- /dev/null +++ b/experimental/apps-mcp/lib/config.go @@ -0,0 +1,57 @@ +// Package mcp provides Model Context Protocol (MCP) server functionality +// integrated into the Databricks CLI. +package mcp + +// Config holds MCP server configuration. +// Configuration is populated from CLI flags and Databricks client context. +type Config struct { + AllowDeployment bool + WithWorkspaceTools bool + WarehouseID string + DatabricksHost string + IoConfig *IoConfig +} + +// IoConfig configures the IO provider for project scaffolding and validation. +type IoConfig struct { + Template *TemplateConfig + Validation *ValidationConfig +} + +// TemplateConfig specifies which template to use for scaffolding new projects. +type TemplateConfig struct { + Name string + Path string +} + +// ValidationConfig defines custom validation commands for project validation. +type ValidationConfig struct { + Command string + Timeout int +} + +// SetDefaults applies default values to ValidationConfig if not explicitly set. +func (v *ValidationConfig) SetDefaults() { + if v.Timeout == 0 { + v.Timeout = 600 + } +} + +// DefaultConfig returns a Config with sensible default values. +func DefaultConfig() *Config { + validationCfg := &ValidationConfig{} + validationCfg.SetDefaults() + + return &Config{ + AllowDeployment: false, + WithWorkspaceTools: false, + IoConfig: &IoConfig{ + Template: &TemplateConfig{ + Name: "default", + Path: "", + }, + Validation: validationCfg, + }, + WarehouseID: "", + } +} diff --git a/experimental/apps-mcp/lib/errors/errors.go b/experimental/apps-mcp/lib/errors/errors.go new file mode 100644 index 0000000000..614558372f --- /dev/null +++ b/experimental/apps-mcp/lib/errors/errors.go @@ -0,0 +1,262 @@ +// Package errors provides JSON-RPC 2.0 compliant error types for the MCP server. +package errors + +import ( + "encoding/json" + "fmt" +) + +// JSON-RPC 2.0 error codes as defined in the specification. +const ( + // CodeParseError indicates invalid JSON was received by the server. + CodeParseError = -32700 + // CodeInvalidRequest indicates the JSON sent is not a valid Request object. + CodeInvalidRequest = -32600 + // CodeMethodNotFound indicates the method does not exist or is not available. + CodeMethodNotFound = -32601 + // CodeInvalidParams indicates invalid method parameter(s). + CodeInvalidParams = -32602 + // CodeInternalError indicates an internal JSON-RPC error. + CodeInternalError = -32603 + // CodeServerError indicates a server-specific error. + CodeServerError = -32000 +) + +// Application-specific error codes (extending JSON-RPC 2.0). +const ( + // Configuration errors (-32100 to -32109) + CodeConfigInvalid = -32100 + CodeConfigMissing = -32101 + CodeConfigWorkDir = -32102 + + // Sandbox errors (-32110 to -32119) + CodeSandboxFailed = -32110 + CodeSandboxNotFound = -32111 + CodeSandboxExec = -32112 + CodeSandboxDocker = -32113 + + // Databricks errors (-32120 to -32129) + CodeDatabricksAuth = -32120 + CodeDatabricksQuery = -32121 + CodeDatabricksWarehouse = -32122 + CodeDatabricksConnect = -32123 + + // Validation errors (-32130 to -32139) + CodeValidationFailed = -32130 + CodeValidationState = -32131 + CodeValidationBuild = -32132 + CodeValidationTest = -32133 + + // Deployment errors (-32140 to -32149) + CodeDeploymentFailed = -32140 + CodeDeploymentState = -32141 + + // State machine errors (-32150 to -32159) + CodeStateTransition = -32150 + CodeStateInvalid = -32151 +) + +// Error represents a structured error following JSON-RPC 2.0 specification. +// It includes an error code, message, and optional details for debugging. +type Error struct { + Code int `json:"code"` + Message string `json:"message"` + Details map[string]any `json:"data,omitempty"` +} + +// Error implements the error interface, formatting the error with its code and message. +func (e *Error) Error() string { + if len(e.Details) > 0 { + details, _ := json.Marshal(e.Details) + return fmt.Sprintf("[%d] %s (details: %s)", e.Code, e.Message, string(details)) + } + return fmt.Sprintf("[%d] %s", e.Code, e.Message) +} + +// WithDetail adds a key-value pair to the error's details map and returns the error for chaining. +// This allows for adding contextual information to errors. +func (e *Error) WithDetail(key string, value any) *Error { + if e.Details == nil { + e.Details = make(map[string]any) + } + e.Details[key] = value + return e +} + +// InvalidParams creates a JSON-RPC 2.0 error indicating invalid method parameters. +// Use this when client-provided parameters fail validation. +func InvalidParams(message string) *Error { + return &Error{ + Code: CodeInvalidParams, + Message: message, + } +} + +// InvalidRequest creates a JSON-RPC 2.0 error for invalid request objects. +// Use this when the JSON sent is not a valid Request object. +func InvalidRequest(message string) *Error { + return &Error{ + Code: CodeInvalidRequest, + Message: message, + } +} + +// InternalError creates a JSON-RPC 2.0 error for internal server errors. +// Use this for unexpected errors that occur during request processing. +func InternalError(message string) *Error { + return &Error{ + Code: CodeInternalError, + Message: message, + } +} + +// MethodNotFound creates a JSON-RPC 2.0 error indicating the requested method doesn't exist. +// The method parameter is included in the error message for debugging. +func MethodNotFound(method string) *Error { + return &Error{ + Code: CodeMethodNotFound, + Message: "method not found: " + method, + } +} + +// ProviderNotAvailable creates a server error indicating a provider is not available. +// The provider name is included both in the message and as a detail for debugging. +func ProviderNotAvailable(provider string) *Error { + err := &Error{ + Code: CodeServerError, + Message: "provider not available: " + provider, + } + return err.WithDetail("provider", provider) +} + +// ParseError creates a JSON-RPC 2.0 error for JSON parsing failures. +// Use this when the server receives invalid JSON. +func ParseError(message string) *Error { + return &Error{ + Code: CodeParseError, + Message: message, + } +} + +// WrapError converts a standard Go error into an Error. +// If the error is already an Error, it is returned as-is. +// Otherwise, it is wrapped as an InternalError with the original error message. +func WrapError(err error) *Error { + if err == nil { + return InternalError("unknown error") + } + if mcpErr, ok := err.(*Error); ok { + return mcpErr + } + return InternalError(err.Error()) +} + +// WithSuggestion adds a helpful suggestion to an error. +// The suggestion appears in the error details under the "suggestion" key. +func WithSuggestion(err *Error, suggestion string) *Error { + if err == nil { + return nil + } + return err.WithDetail("suggestion", suggestion) +} + +// NewWithCode creates a new error with a specific code and message. +func NewWithCode(code int, message string) *Error { + return &Error{ + Code: code, + Message: message, + } +} + +// Configuration error helpers + +// ConfigInvalid creates an error for invalid configuration. +func ConfigInvalid(message string) *Error { + return NewWithCode(CodeConfigInvalid, message) +} + +// ConfigMissing creates an error for missing configuration. +func ConfigMissing(param string) *Error { + err := NewWithCode(CodeConfigMissing, "missing required configuration: "+param) + return WithSuggestion(err, "Check your ~/.databricks/apps-mcp/config.json or environment variables") +} + +// ConfigWorkDir creates an error for workspace directory issues. +func ConfigWorkDir(message string) *Error { + return NewWithCode(CodeConfigWorkDir, message) +} + +// Sandbox error helpers + +// SandboxFailed creates an error for sandbox operation failures. +func SandboxFailed(operation, message string) *Error { + err := NewWithCode(CodeSandboxFailed, fmt.Sprintf("sandbox %s failed: %s", operation, message)) + return err.WithDetail("operation", operation) +} + +// SandboxDocker creates an error for Docker-related issues. +func SandboxDocker(message string) *Error { + err := NewWithCode(CodeSandboxDocker, message) + return WithSuggestion(err, "Ensure Docker is running: docker info") +} + +// Databricks error helpers + +// DatabricksAuth creates an error for authentication failures. +func DatabricksAuth(message string) *Error { + err := NewWithCode(CodeDatabricksAuth, message) + return WithSuggestion(err, "Check that DATABRICKS_HOST and DATABRICKS_TOKEN are set correctly") +} + +// DatabricksWarehouse creates an error for warehouse access issues. +func DatabricksWarehouse(warehouseID string) *Error { + err := NewWithCode(CodeDatabricksWarehouse, "warehouse not accessible: "+warehouseID) + return WithSuggestion(err, "Verify the warehouse ID and ensure it is running") +} + +// DatabricksConnect creates an error for connection failures. +func DatabricksConnect(message string) *Error { + err := NewWithCode(CodeDatabricksConnect, message) + return WithSuggestion(err, "Check network connectivity and DATABRICKS_HOST configuration") +} + +// Validation error helpers + +// ValidationFailed creates an error for validation failures. +func ValidationFailed(step, message string) *Error { + err := NewWithCode(CodeValidationFailed, fmt.Sprintf("validation failed at %s: %s", step, message)) + return err.WithDetail("step", step) +} + +// ValidationState creates an error for invalid project state during validation. +func ValidationState(currentState, message string) *Error { + err := NewWithCode(CodeValidationState, message) + return err.WithDetail("current_state", currentState) +} + +// Deployment error helpers + +// DeploymentFailed creates an error for deployment failures. +func DeploymentFailed(message string) *Error { + return NewWithCode(CodeDeploymentFailed, message) +} + +// DeploymentState creates an error for invalid project state during deployment. +func DeploymentState(currentState, message string) *Error { + err := NewWithCode(CodeDeploymentState, message) + err = err.WithDetail("current_state", currentState) + return WithSuggestion(err, "Ensure the project is validated before deployment") +} + +// State machine error helpers + +// StateTransition creates an error for invalid state transitions. +func StateTransition(from, to, message string) *Error { + err := NewWithCode(CodeStateTransition, fmt.Sprintf("invalid state transition %s -> %s: %s", from, to, message)) + return err.WithDetail("from_state", from).WithDetail("to_state", to) +} + +// StateInvalid creates an error for invalid state values. +func StateInvalid(state string) *Error { + return NewWithCode(CodeStateInvalid, "invalid state: "+state) +} diff --git a/experimental/apps-mcp/lib/errors/errors_test.go b/experimental/apps-mcp/lib/errors/errors_test.go new file mode 100644 index 0000000000..4d82269f8c --- /dev/null +++ b/experimental/apps-mcp/lib/errors/errors_test.go @@ -0,0 +1,272 @@ +package errors + +import ( + "errors" + "testing" +) + +func TestError_Error(t *testing.T) { + tests := []struct { + name string + err *Error + expected string + }{ + { + name: "error without details", + err: &Error{ + Code: CodeInternalError, + Message: "something went wrong", + }, + expected: "[-32603] something went wrong", + }, + { + name: "error with details", + err: &Error{ + Code: CodeInvalidParams, + Message: "invalid parameter", + Details: map[string]any{ + "field": "username", + "value": "invalid", + }, + }, + expected: `[-32602] invalid parameter (details: {"field":"username","value":"invalid"})`, + }, + { + name: "error with empty details map", + err: &Error{ + Code: CodeInternalError, + Message: "error", + Details: map[string]any{}, + }, + expected: "[-32603] error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.err.Error() + if got != tt.expected { + t.Errorf("Error() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestError_WithDetail(t *testing.T) { + t.Run("add detail to error without details", func(t *testing.T) { + err := &Error{ + Code: CodeInternalError, + Message: "error", + } + + result := err.WithDetail("key", "value") + + if result != err { + t.Error("WithDetail should return the same error instance") + } + + if len(err.Details) != 1 { + t.Errorf("Details length = %d, want 1", len(err.Details)) + } + + if err.Details["key"] != "value" { + t.Errorf("Details[key] = %v, want %q", err.Details["key"], "value") + } + }) + + t.Run("add detail to error with existing details", func(t *testing.T) { + err := &Error{ + Code: CodeInternalError, + Message: "error", + Details: map[string]any{ + "existing": "value", + }, + } + + err = err.WithDetail("new", "data") + + if len(err.Details) != 2 { + t.Errorf("Details length = %d, want 2", len(err.Details)) + } + + if err.Details["existing"] != "value" { + t.Error("existing detail should be preserved") + } + + if err.Details["new"] != "data" { + t.Error("new detail should be added") + } + }) + + t.Run("chain multiple details", func(t *testing.T) { + err := &Error{ + Code: CodeInternalError, + Message: "error", + } + + err = err.WithDetail("key1", "value1").WithDetail("key2", "value2") + + if len(err.Details) != 2 { + t.Errorf("Details length = %d, want 2", len(err.Details)) + } + }) +} + +func TestInvalidParams(t *testing.T) { + message := "missing required field" + err := InvalidParams(message) + + if err.Code != CodeInvalidParams { + t.Errorf("Code = %d, want %d", err.Code, CodeInvalidParams) + } + + if err.Message != message { + t.Errorf("Message = %q, want %q", err.Message, message) + } + + if err.Details != nil { + t.Error("Details should be nil by default") + } +} + +func TestInvalidRequest(t *testing.T) { + message := "malformed request" + err := InvalidRequest(message) + + if err.Code != CodeInvalidRequest { + t.Errorf("Code = %d, want %d", err.Code, CodeInvalidRequest) + } + + if err.Message != message { + t.Errorf("Message = %q, want %q", err.Message, message) + } +} + +func TestInternalError(t *testing.T) { + message := "internal server error" + err := InternalError(message) + + if err.Code != CodeInternalError { + t.Errorf("Code = %d, want %d", err.Code, CodeInternalError) + } + + if err.Message != message { + t.Errorf("Message = %q, want %q", err.Message, message) + } +} + +func TestMethodNotFound(t *testing.T) { + method := "unknown_method" + err := MethodNotFound(method) + + if err.Code != CodeMethodNotFound { + t.Errorf("Code = %d, want %d", err.Code, CodeMethodNotFound) + } + + expectedMessage := "method not found: unknown_method" + if err.Message != expectedMessage { + t.Errorf("Message = %q, want %q", err.Message, expectedMessage) + } +} + +func TestProviderNotAvailable(t *testing.T) { + provider := "databricks" + err := ProviderNotAvailable(provider) + + if err.Code != CodeServerError { + t.Errorf("Code = %d, want %d", err.Code, CodeServerError) + } + + expectedMessage := "provider not available: databricks" + if err.Message != expectedMessage { + t.Errorf("Message = %q, want %q", err.Message, expectedMessage) + } + + if err.Details == nil { + t.Fatal("Details should not be nil") + } + + if err.Details["provider"] != provider { + t.Errorf("Details[provider] = %v, want %q", err.Details["provider"], provider) + } +} + +func TestParseError(t *testing.T) { + message := "failed to parse JSON" + err := ParseError(message) + + if err.Code != CodeParseError { + t.Errorf("Code = %d, want %d", err.Code, CodeParseError) + } + + if err.Message != message { + t.Errorf("Message = %q, want %q", err.Message, message) + } +} + +func TestWrapError(t *testing.T) { + t.Run("wrap nil error", func(t *testing.T) { + err := WrapError(nil) + + if err.Code != CodeInternalError { + t.Errorf("Code = %d, want %d", err.Code, CodeInternalError) + } + + if err.Message != "unknown error" { + t.Errorf("Message = %q, want %q", err.Message, "unknown error") + } + }) + + t.Run("wrap standard error", func(t *testing.T) { + originalErr := errors.New("standard error") + err := WrapError(originalErr) + + if err.Code != CodeInternalError { + t.Errorf("Code = %d, want %d", err.Code, CodeInternalError) + } + + if err.Message != "standard error" { + t.Errorf("Message = %q, want %q", err.Message, "standard error") + } + }) + + t.Run("wrap Error returns same error", func(t *testing.T) { + originalErr := &Error{ + Code: CodeInvalidParams, + Message: "invalid params", + } + + err := WrapError(originalErr) + + if err != originalErr { + t.Error("WrapError should return the same Error instance") + } + + if err.Code != CodeInvalidParams { + t.Errorf("Code = %d, want %d", err.Code, CodeInvalidParams) + } + }) +} + +func TestErrorCodes(t *testing.T) { + tests := []struct { + name string + code int + expected int + }{ + {"ParseError", CodeParseError, -32700}, + {"InvalidRequest", CodeInvalidRequest, -32600}, + {"MethodNotFound", CodeMethodNotFound, -32601}, + {"InvalidParams", CodeInvalidParams, -32602}, + {"InternalError", CodeInternalError, -32603}, + {"ServerError", CodeServerError, -32000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.code != tt.expected { + t.Errorf("%s = %d, want %d", tt.name, tt.code, tt.expected) + } + }) + } +} diff --git a/experimental/apps-mcp/lib/fileutil/atomic.go b/experimental/apps-mcp/lib/fileutil/atomic.go new file mode 100644 index 0000000000..e594d8b8c7 --- /dev/null +++ b/experimental/apps-mcp/lib/fileutil/atomic.go @@ -0,0 +1,33 @@ +// Package fileutil provides file operation utilities. +package fileutil + +import ( + "fmt" + "os" + "path/filepath" +) + +// AtomicWriteFile writes content to a file atomically by writing to a temporary file +// first and then renaming it to the target path. This ensures that the file is never +// left in a partially written state. +// +// The function creates parent directories if they don't exist and cleans up the +// temporary file on error. +func AtomicWriteFile(path string, content []byte, perm os.FileMode) error { + parentDir := filepath.Dir(path) + if err := os.MkdirAll(parentDir, 0o755); err != nil { + return fmt.Errorf("failed to create parent directory: %w", err) + } + + tempPath := path + ".tmp" + if err := os.WriteFile(tempPath, content, perm); err != nil { + return fmt.Errorf("failed to write temp file: %w", err) + } + + if err := os.Rename(tempPath, path); err != nil { + os.Remove(tempPath) + return fmt.Errorf("failed to rename temp file: %w", err) + } + + return nil +} diff --git a/experimental/apps-mcp/lib/fileutil/atomic_test.go b/experimental/apps-mcp/lib/fileutil/atomic_test.go new file mode 100644 index 0000000000..785545839f --- /dev/null +++ b/experimental/apps-mcp/lib/fileutil/atomic_test.go @@ -0,0 +1,133 @@ +package fileutil + +import ( + "os" + "path/filepath" + "testing" +) + +func TestAtomicWriteFile(t *testing.T) { + t.Run("write file successfully", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.txt") + content := []byte("test content") + + err := AtomicWriteFile(filePath, content, 0o644) + if err != nil { + t.Fatalf("AtomicWriteFile() error = %v", err) + } + + got, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + if string(got) != string(content) { + t.Errorf("file content = %q, want %q", string(got), string(content)) + } + + info, err := os.Stat(filePath) + if err != nil { + t.Fatalf("failed to stat file: %v", err) + } + + if info.Mode().Perm() != 0o644 { + t.Errorf("file mode = %o, want %o", info.Mode().Perm(), 0o644) + } + }) + + t.Run("create parent directories", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "a", "b", "c", "test.txt") + content := []byte("nested content") + + err := AtomicWriteFile(filePath, content, 0o644) + if err != nil { + t.Fatalf("AtomicWriteFile() error = %v", err) + } + + got, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + if string(got) != string(content) { + t.Errorf("file content = %q, want %q", string(got), string(content)) + } + }) + + t.Run("overwrite existing file", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.txt") + + err := os.WriteFile(filePath, []byte("old content"), 0o644) + if err != nil { + t.Fatalf("failed to create initial file: %v", err) + } + + newContent := []byte("new content") + err = AtomicWriteFile(filePath, newContent, 0o644) + if err != nil { + t.Fatalf("AtomicWriteFile() error = %v", err) + } + + got, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + if string(got) != string(newContent) { + t.Errorf("file content = %q, want %q", string(got), string(newContent)) + } + }) + + t.Run("cleanup temp file on rename failure", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "readonly", "test.txt") + + if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { + t.Fatalf("failed to create directory: %v", err) + } + + if err := os.WriteFile(filePath, []byte("locked"), 0o644); err != nil { + t.Fatalf("failed to create file: %v", err) + } + + if err := os.Chmod(filepath.Dir(filePath), 0o555); err != nil { + t.Fatalf("failed to set read-only: %v", err) + } + defer func() { + _ = os.Chmod(filepath.Dir(filePath), 0o755) + }() + + err := AtomicWriteFile(filePath, []byte("new content"), 0o644) + if err == nil { + t.Fatal("expected error, got nil") + } + + tempPath := filePath + ".tmp" + if _, err := os.Stat(tempPath); !os.IsNotExist(err) { + t.Errorf("temp file should be cleaned up, but exists") + } + }) + + t.Run("preserve file permissions", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "test.txt") + content := []byte("test content") + + err := AtomicWriteFile(filePath, content, 0o600) + if err != nil { + t.Fatalf("AtomicWriteFile() error = %v", err) + } + + info, err := os.Stat(filePath) + if err != nil { + t.Fatalf("failed to stat file: %v", err) + } + + if info.Mode().Perm() != 0o600 { + t.Errorf("file mode = %o, want %o", info.Mode().Perm(), 0o600) + } + }) +} diff --git a/experimental/apps-mcp/lib/mcp/protocol.go b/experimental/apps-mcp/lib/mcp/protocol.go new file mode 100644 index 0000000000..a01d0059ce --- /dev/null +++ b/experimental/apps-mcp/lib/mcp/protocol.go @@ -0,0 +1,76 @@ +package mcp + +import "encoding/json" + +// JSONRPCRequest represents a JSON-RPC 2.0 request. +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +// JSONRPCResponse represents a JSON-RPC 2.0 response. +type JSONRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id,omitempty"` + Result json.RawMessage `json:"result,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` +} + +// JSONRPCError represents a JSON-RPC 2.0 error. +type JSONRPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` +} + +// MCP protocol constants. +const ( + MethodInitialize = "initialize" + MethodToolsList = "tools/list" + MethodToolsCall = "tools/call" + MethodPing = "ping" + MethodNotification = "notifications/initialized" +) + +// InitializeRequest represents an initialize request. +type InitializeRequest struct { + ProtocolVersion string `json:"protocolVersion"` + Capabilities map[string]any `json:"capabilities"` + ClientInfo Implementation `json:"clientInfo"` +} + +// InitializeResult represents an initialize response. +type InitializeResult struct { + ProtocolVersion string `json:"protocolVersion"` + Capabilities ServerCapabilities `json:"capabilities"` + ServerInfo Implementation `json:"serverInfo"` +} + +// ServerCapabilities describes server capabilities. +type ServerCapabilities struct { + Tools *ToolsCapability `json:"tools,omitempty"` +} + +// ToolsCapability describes tool capabilities. +type ToolsCapability struct { + ListChanged bool `json:"listChanged,omitempty"` +} + +// ListToolsResult represents the result of listing tools. +type ListToolsResult struct { + Tools []Tool `json:"tools"` +} + +// CallToolParams represents parameters for calling a tool. +type CallToolParams struct { + Name string `json:"name"` + Arguments json.RawMessage `json:"arguments,omitempty"` +} + +// CallToolResult represents the result of calling a tool. +type CallToolResult struct { + Content []Content `json:"content"` + IsError bool `json:"isError,omitempty"` +} diff --git a/experimental/apps-mcp/lib/mcp/server.go b/experimental/apps-mcp/lib/mcp/server.go new file mode 100644 index 0000000000..419dc6d31f --- /dev/null +++ b/experimental/apps-mcp/lib/mcp/server.go @@ -0,0 +1,250 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + "io" + "sync" +) + +// Server is an MCP server that manages tools and handles requests. +type Server struct { + impl *Implementation + tools map[string]*serverTool + toolsMu sync.RWMutex + transport *StdioTransport + initialized bool +} + +// serverTool represents a registered tool with its handler. +type serverTool struct { + tool *Tool + handler ToolHandler +} + +// NewServer creates a new MCP server. +func NewServer(impl *Implementation, options any) *Server { + return &Server{ + impl: impl, + tools: make(map[string]*serverTool), + } +} + +// AddTool registers a tool with a low-level handler. +// This is the internal method used by the typed AddTool function. +func (s *Server) AddTool(tool *Tool, handler ToolHandler) { + s.toolsMu.Lock() + defer s.toolsMu.Unlock() + + s.tools[tool.Name] = &serverTool{ + tool: tool, + handler: handler, + } +} + +// Run starts the MCP server with the given transport. +func (s *Server) Run(ctx context.Context, transport *StdioTransport) error { + s.transport = transport + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + req, err := transport.Read(ctx) + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + resp := s.handleRequest(ctx, req) + if resp != nil { + if err := transport.Write(ctx, resp); err != nil { + return err + } + } + } +} + +// handleRequest processes a JSON-RPC request and returns a response. +func (s *Server) handleRequest(ctx context.Context, req *JSONRPCRequest) *JSONRPCResponse { + switch req.Method { + case MethodInitialize: + return s.handleInitialize(req) + case MethodToolsList: + return s.handleToolsList(req) + case MethodToolsCall: + return s.handleToolsCall(ctx, req) + case MethodPing: + return s.handlePing(req) + case MethodNotification: + // Notifications don't require a response + s.initialized = true + return nil + default: + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: -32601, + Message: "method not found: " + req.Method, + }, + } + } +} + +// handleInitialize handles the initialize request. +func (s *Server) handleInitialize(req *JSONRPCRequest) *JSONRPCResponse { + result := InitializeResult{ + ProtocolVersion: "2024-11-05", + Capabilities: ServerCapabilities{ + Tools: &ToolsCapability{ + ListChanged: false, + }, + }, + ServerInfo: *s.impl, + } + + data, err := json.Marshal(result) + if err != nil { + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: -32603, + Message: fmt.Sprintf("failed to marshal result: %v", err), + }, + } + } + + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: data, + } +} + +// handleToolsList handles the tools/list request. +func (s *Server) handleToolsList(req *JSONRPCRequest) *JSONRPCResponse { + s.toolsMu.RLock() + defer s.toolsMu.RUnlock() + + tools := make([]Tool, 0, len(s.tools)) + for _, st := range s.tools { + tools = append(tools, *st.tool) + } + + result := ListToolsResult{ + Tools: tools, + } + + data, err := json.Marshal(result) + if err != nil { + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: -32603, + Message: fmt.Sprintf("failed to marshal result: %v", err), + }, + } + } + + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: data, + } +} + +// handleToolsCall handles the tools/call request. +func (s *Server) handleToolsCall(ctx context.Context, req *JSONRPCRequest) *JSONRPCResponse { + var params CallToolParams + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: -32602, + Message: fmt.Sprintf("invalid params: %v", err), + }, + } + } + + s.toolsMu.RLock() + st, ok := s.tools[params.Name] + s.toolsMu.RUnlock() + + if !ok { + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: -32602, + Message: "tool not found: " + params.Name, + }, + } + } + + toolReq := &CallToolRequest{ + Params: params, + } + + result, err := st.handler(ctx, toolReq) + if err != nil { + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: -32603, + Message: fmt.Sprintf("tool execution error: %v", err), + }, + } + } + + // Convert Content slice to []any for JSON marshaling + content := make([]any, len(result.Content)) + for i, c := range result.Content { + content[i] = c + } + + resultData := struct { + Content []any `json:"content"` + IsError bool `json:"isError,omitempty"` + }{ + Content: content, + IsError: result.IsError, + } + + data, err := json.Marshal(resultData) + if err != nil { + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: -32603, + Message: fmt.Sprintf("failed to marshal result: %v", err), + }, + } + } + + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: data, + } +} + +// handlePing handles the ping request. +func (s *Server) handlePing(req *JSONRPCRequest) *JSONRPCResponse { + return &JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage("{}"), + } +} diff --git a/experimental/apps-mcp/lib/mcp/tool.go b/experimental/apps-mcp/lib/mcp/tool.go new file mode 100644 index 0000000000..e0ea30fbeb --- /dev/null +++ b/experimental/apps-mcp/lib/mcp/tool.go @@ -0,0 +1,99 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + + "github.com/google/jsonschema-go/jsonschema" +) + +// AddTool registers a typed tool handler with automatic schema generation and validation. +// This is the high-level API that most users should use. +// +// The In type provides the input schema, and Out provides the output schema. +// Input validation is automatic. Output is automatically converted to Content. +func AddTool[In, Out any]( + server *Server, + tool *Tool, + handler ToolHandlerFor[In, Out], +) { + // Generate input schema if not provided + if tool.InputSchema == nil { + schema, err := jsonschema.For[In](nil) + if err != nil { + panic(fmt.Errorf("failed to generate input schema for tool %s: %w", tool.Name, err)) + } + tool.InputSchema = schema + } + + // Create a low-level handler that wraps the typed handler + lowLevelHandler := func(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) { + // Unmarshal input + var input In + if len(req.Params.Arguments) > 0 { + if err := json.Unmarshal(req.Params.Arguments, &input); err != nil { + return nil, fmt.Errorf("invalid input: %w", err) + } + } + + // Call the typed handler + result, output, err := handler(ctx, req, input) + // If there's an error, wrap it in a tool result + if err != nil { + content := &TextContent{ + Type: "text", + Text: fmt.Sprintf("Error: %v", err), + } + return &CallToolResult{ + Content: []Content{content}, + IsError: true, + }, nil + } + + // If result is nil, create a default result from output + if result == nil { + result = &CallToolResult{} + } + + // If content is empty, generate content from output + if len(result.Content) == 0 { + // Check if output is the zero value + outputValue := reflect.ValueOf(output) + if !isZeroValue(outputValue) { + // Marshal output to JSON + outputJSON, err := json.MarshalIndent(output, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal output: %w", err) + } + + content := &TextContent{ + Type: "text", + Text: string(outputJSON), + } + result.Content = []Content{content} + } + } + + return result, nil + } + + // Register the tool with the server + server.AddTool(tool, lowLevelHandler) +} + +// isZeroValue checks if a reflect.Value is the zero value for its type. +func isZeroValue(v reflect.Value) bool { + if !v.IsValid() { + return true + } + + switch v.Kind() { + case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func: + return v.IsNil() + default: + zero := reflect.Zero(v.Type()) + return v.Interface() == zero.Interface() + } +} diff --git a/experimental/apps-mcp/lib/mcp/transport.go b/experimental/apps-mcp/lib/mcp/transport.go new file mode 100644 index 0000000000..ecd5017f1f --- /dev/null +++ b/experimental/apps-mcp/lib/mcp/transport.go @@ -0,0 +1,61 @@ +package mcp + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os" + "sync" +) + +// StdioTransport implements MCP over stdio using line-delimited JSON. +type StdioTransport struct { + reader *bufio.Reader + writer io.Writer + mu sync.Mutex +} + +// NewStdioTransport creates a new stdio transport. +func NewStdioTransport() *StdioTransport { + return &StdioTransport{ + reader: bufio.NewReader(os.Stdin), + writer: os.Stdout, + } +} + +// Read reads a JSON-RPC message from stdin. +func (t *StdioTransport) Read(ctx context.Context) (*JSONRPCRequest, error) { + line, err := t.reader.ReadBytes('\n') + if err != nil { + if err == io.EOF { + return nil, io.EOF + } + return nil, fmt.Errorf("failed to read from stdin: %w", err) + } + + var req JSONRPCRequest + if err := json.Unmarshal(line, &req); err != nil { + return nil, fmt.Errorf("failed to unmarshal request: %w", err) + } + + return &req, nil +} + +// Write writes a JSON-RPC response to stdout. +func (t *StdioTransport) Write(ctx context.Context, resp *JSONRPCResponse) error { + t.mu.Lock() + defer t.mu.Unlock() + + data, err := json.Marshal(resp) + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) + } + + if _, err := t.writer.Write(append(data, '\n')); err != nil { + return fmt.Errorf("failed to write to stdout: %w", err) + } + + return nil +} diff --git a/experimental/apps-mcp/lib/mcp/types.go b/experimental/apps-mcp/lib/mcp/types.go new file mode 100644 index 0000000000..5ce3c98bbb --- /dev/null +++ b/experimental/apps-mcp/lib/mcp/types.go @@ -0,0 +1,43 @@ +// Package mcp provides a minimal implementation of the Model Context Protocol for stdio-based servers. +package mcp + +import ( + "context" +) + +// Implementation represents server or client implementation details. +type Implementation struct { + Name string `json:"name"` + Version string `json:"version"` +} + +// Tool represents an MCP tool definition. +type Tool struct { + Name string `json:"name"` + Description string `json:"description"` + InputSchema any `json:"inputSchema"` +} + +// CallToolRequest represents a request to call a tool. +type CallToolRequest struct { + Params CallToolParams +} + +// Content represents content in a tool result. +type Content interface { + isContent() +} + +// TextContent represents text content. +type TextContent struct { + Type string `json:"type"` + Text string `json:"text"` +} + +func (t *TextContent) isContent() {} + +// ToolHandler is a low-level handler for tool calls. +type ToolHandler func(context.Context, *CallToolRequest) (*CallToolResult, error) + +// ToolHandlerFor is a typed handler for tool calls with automatic marshaling. +type ToolHandlerFor[In, Out any] func(context.Context, *CallToolRequest, In) (*CallToolResult, Out, error) diff --git a/experimental/apps-mcp/lib/pathutil/pathutil.go b/experimental/apps-mcp/lib/pathutil/pathutil.go new file mode 100644 index 0000000000..2a0c20dfbb --- /dev/null +++ b/experimental/apps-mcp/lib/pathutil/pathutil.go @@ -0,0 +1,144 @@ +package pathutil + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// ValidatePath ensures that the user-provided path is within baseDir and +// returns the absolute path. This prevents directory traversal attacks. +// +// Security checks: +// - Resolves relative paths against baseDir +// - Cleans the path (removes .., ., etc.) +// - Resolves symlinks to prevent symlink escape attacks +// - Verifies the final path is within baseDir +// +// For non-existent paths, validates that the parent directory exists and +// is within baseDir. +func ValidatePath(baseDir, userPath string) (string, error) { + if filepath.IsAbs(userPath) { + return "", fmt.Errorf("absolute paths not allowed: %s", userPath) + } + + base, err := resolveBasePath(baseDir) + if err != nil { + return "", err + } + + target := filepath.Join(base, userPath) + cleaned := filepath.Clean(target) + + resolved, err := resolveTargetPath(cleaned) + if err != nil { + return "", err + } + + if err := checkPathWithinBase(base, resolved); err != nil { + return "", err + } + + return resolved, nil +} + +// resolveBasePath resolves the base directory to its absolute canonical path. +func resolveBasePath(baseDir string) (string, error) { + base, err := filepath.Abs(baseDir) + if err != nil { + return "", fmt.Errorf("failed to get absolute base path: %w", err) + } + + baseResolved, err := filepath.EvalSymlinks(base) + if err != nil { + return "", fmt.Errorf("failed to resolve base directory symlinks: %w", err) + } + + base = filepath.Clean(baseResolved) + if !strings.HasSuffix(base, string(filepath.Separator)) { + base += string(filepath.Separator) + } + + return base, nil +} + +// resolveTargetPath resolves the target path, handling both existing and non-existent paths. +func resolveTargetPath(cleaned string) (string, error) { + _, err := os.Lstat(cleaned) + if err == nil { + return resolveExistingPath(cleaned) + } + + if os.IsNotExist(err) { + return resolveNonExistentPath(cleaned) + } + + return "", fmt.Errorf("failed to stat path: %w", err) +} + +// resolveExistingPath resolves symlinks for an existing path. +func resolveExistingPath(path string) (string, error) { + resolved, err := filepath.EvalSymlinks(path) + if err != nil { + return "", fmt.Errorf("failed to resolve symlink: %w", err) + } + return resolved, nil +} + +// resolveNonExistentPath validates non-existent paths by checking the parent directory. +func resolveNonExistentPath(cleaned string) (string, error) { + parent := filepath.Dir(cleaned) + + _, err := os.Stat(parent) + if os.IsNotExist(err) { + return cleaned, nil + } + if err != nil { + return "", fmt.Errorf("failed to stat parent directory: %w", err) + } + + parentResolved, err := filepath.EvalSymlinks(parent) + if err != nil { + return "", fmt.Errorf("failed to resolve parent symlink: %w", err) + } + + return filepath.Join(parentResolved, filepath.Base(cleaned)), nil +} + +// checkPathWithinBase verifies that the resolved path is within the base directory. +func checkPathWithinBase(base, resolved string) error { + if !strings.HasPrefix(resolved+string(filepath.Separator), base) { + return fmt.Errorf("path outside base directory: %s not in %s", resolved, base) + } + return nil +} + +// MustValidatePath is like ValidatePath but panics on error. +// Use this only in tests or when you know the path is safe. +func MustValidatePath(baseDir, userPath string) string { + path, err := ValidatePath(baseDir, userPath) + if err != nil { + panic(err) + } + return path +} + +// RelativePath returns the relative path from baseDir to targetPath. +// Both paths should be absolute. Returns an error if targetPath is not +// within baseDir. +func RelativePath(baseDir, targetPath string) (string, error) { + base := filepath.Clean(baseDir) + target := filepath.Clean(targetPath) + + if !strings.HasPrefix(target, base) { + return "", fmt.Errorf("target path %s is not within base %s", target, base) + } + + rel, err := filepath.Rel(base, target) + if err != nil { + return "", fmt.Errorf("failed to compute relative path: %w", err) + } + + return rel, nil +} diff --git a/experimental/apps-mcp/lib/pathutil/pathutil_test.go b/experimental/apps-mcp/lib/pathutil/pathutil_test.go new file mode 100644 index 0000000000..edaeadda05 --- /dev/null +++ b/experimental/apps-mcp/lib/pathutil/pathutil_test.go @@ -0,0 +1,167 @@ +package pathutil + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestValidatePath(t *testing.T) { + baseDir := t.TempDir() + + tests := []struct { + name string + userPath string + wantErr bool + errMsg string + setup func() error + }{ + { + name: "simple relative path", + userPath: "test.txt", + wantErr: false, + }, + { + name: "nested relative path", + userPath: "subdir/test.txt", + wantErr: false, + }, + { + name: "path with dots", + userPath: "subdir/./test.txt", + wantErr: false, + }, + { + name: "path traversal attempt", + userPath: "../outside.txt", + wantErr: true, + errMsg: "outside base directory", + }, + { + name: "absolute path traversal", + userPath: "/../etc/passwd", + wantErr: true, + errMsg: "absolute paths not allowed", + }, + { + name: "symlink escape attempt", + userPath: "symlink/test.txt", + wantErr: true, + errMsg: "outside base directory", + setup: func() error { + linkPath := filepath.Join(baseDir, "symlink") + return os.Symlink("/tmp", linkPath) + }, + }, + { + name: "valid symlink within base", + userPath: "goodlink/test.txt", + wantErr: false, + setup: func() error { + targetDir := filepath.Join(baseDir, "target") + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return err + } + linkPath := filepath.Join(baseDir, "goodlink") + return os.Symlink(targetDir, linkPath) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + if err := tt.setup(); err != nil { + t.Fatalf("setup failed: %v", err) + } + } + + result, err := ValidatePath(baseDir, tt.userPath) + + if tt.wantErr { + if err == nil { + t.Errorf("ValidatePath() expected error containing %q, got nil", tt.errMsg) + } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("ValidatePath() error = %v, want error containing %q", err, tt.errMsg) + } + } else { + if err != nil { + t.Errorf("ValidatePath() unexpected error: %v", err) + } + if !filepath.IsAbs(result) { + t.Errorf("ValidatePath() result %q is not absolute", result) + } + absBase, _ := filepath.Abs(baseDir) + if !strings.Contains(result, absBase) { + t.Errorf("ValidatePath() result %q does not start with base %q", result, absBase) + } + } + }) + } +} + +func TestRelativePath(t *testing.T) { + tests := []struct { + name string + baseDir string + targetPath string + want string + wantErr bool + }{ + { + name: "simple relative path", + baseDir: "/tmp/base", + targetPath: "/tmp/base/file.txt", + want: "file.txt", + wantErr: false, + }, + { + name: "nested relative path", + baseDir: "/tmp/base", + targetPath: "/tmp/base/sub/dir/file.txt", + want: "sub/dir/file.txt", + wantErr: false, + }, + { + name: "outside base directory", + baseDir: "/tmp/base", + targetPath: "/tmp/other/file.txt", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := RelativePath(tt.baseDir, tt.targetPath) + if tt.wantErr { + if err == nil { + t.Errorf("RelativePath() expected error, got nil") + } + } else { + if err != nil { + t.Errorf("RelativePath() unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("RelativePath() = %q, want %q", got, tt.want) + } + } + }) + } +} + +func TestMustValidatePath(t *testing.T) { + baseDir := t.TempDir() + + result := MustValidatePath(baseDir, "test.txt") + if result == "" { + t.Error("MustValidatePath() returned empty string") + } + + defer func() { + if r := recover(); r == nil { + t.Error("MustValidatePath() should panic for invalid path") + } + }() + MustValidatePath(baseDir, "../outside.txt") +} diff --git a/experimental/apps-mcp/lib/providers/databricks/catalogs.go b/experimental/apps-mcp/lib/providers/databricks/catalogs.go new file mode 100644 index 0000000000..03c68d1528 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/databricks/catalogs.go @@ -0,0 +1,162 @@ +package databricks + +import ( + "context" + "fmt" + "strings" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// ListCatalogsResult represents the result of listing catalogs +type ListCatalogsResult struct { + Catalogs []string `json:"catalogs"` +} + +// ListCatalogs lists all available Databricks Unity Catalog catalogs +func ListCatalogs(ctx context.Context, cfg *mcp.Config) (*ListCatalogsResult, error) { + w := cmdctx.WorkspaceClient(ctx) + catalogs, err := w.Catalogs.ListAll(ctx, catalog.ListCatalogsRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to list catalogs: %w", err) + } + + names := make([]string, len(catalogs)) + for i, cat := range catalogs { + names[i] = cat.Name + } + + return &ListCatalogsResult{Catalogs: names}, nil +} + +// ListSchemasArgs represents arguments for listing schemas +type ListSchemasArgs struct { + CatalogName string `json:"catalog_name"` + Filter string `json:"filter,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` +} + +// ListSchemasResult represents the result of listing schemas +type ListSchemasResult struct { + Schemas []string `json:"schemas"` + TotalCount int `json:"total_count"` + ShownCount int `json:"shown_count"` + Offset int `json:"offset"` + Limit int `json:"limit"` +} + +// ListSchemas lists schemas in a catalog with optional filtering and pagination +func ListSchemas(ctx context.Context, cfg *mcp.Config, args *ListSchemasArgs) (*ListSchemasResult, error) { + if args.Limit == 0 { + args.Limit = 1000 + } + + w := cmdctx.WorkspaceClient(ctx) + schemas, err := w.Schemas.ListAll(ctx, catalog.ListSchemasRequest{ + CatalogName: args.CatalogName, + }) + if err != nil { + return nil, fmt.Errorf("failed to list schemas: %w", err) + } + + // Extract names + names := make([]string, len(schemas)) + for i, schema := range schemas { + names[i] = schema.Name + } + + // Apply filter if provided + if args.Filter != "" { + var filtered []string + filterLower := strings.ToLower(args.Filter) + for _, name := range names { + if strings.Contains(strings.ToLower(name), filterLower) { + filtered = append(filtered, name) + } + } + names = filtered + } + + // Apply pagination + totalCount := len(names) + start := args.Offset + end := start + args.Limit + + if start > len(names) { + start = len(names) + } + if end > len(names) { + end = len(names) + } + + paginatedNames := names[start:end] + + return &ListSchemasResult{ + Schemas: paginatedNames, + TotalCount: totalCount, + ShownCount: len(paginatedNames), + Offset: args.Offset, + Limit: args.Limit, + }, nil +} + +// ListTablesArgs represents arguments for listing tables +type ListTablesArgs struct { + CatalogName string `json:"catalog_name"` + SchemaName string `json:"schema_name"` + ExcludeInaccessible bool `json:"exclude_inaccessible"` +} + +// TableInfo represents information about a table +type TableInfo struct { + Name string `json:"name"` + CatalogName string `json:"catalog_name"` + SchemaName string `json:"schema_name"` + FullName string `json:"full_name"` + TableType string `json:"table_type"` + Owner *string `json:"owner,omitempty"` + Comment *string `json:"comment,omitempty"` +} + +// ListTablesResult represents the result of listing tables +type ListTablesResult struct { + Tables []TableInfo `json:"tables"` +} + +// ListTables lists tables in a schema +func ListTables(ctx context.Context, cfg *mcp.Config, args *ListTablesArgs) (*ListTablesResult, error) { + w := cmdctx.WorkspaceClient(ctx) + tables, err := w.Tables.ListAll(ctx, catalog.ListTablesRequest{ + CatalogName: args.CatalogName, + SchemaName: args.SchemaName, + IncludeBrowse: !args.ExcludeInaccessible, + }) + if err != nil { + return nil, fmt.Errorf("failed to list tables: %w", err) + } + + infos := make([]TableInfo, len(tables)) + for i, table := range tables { + var owner, comment *string + if table.Owner != "" { + owner = &table.Owner + } + if table.Comment != "" { + comment = &table.Comment + } + infos[i] = TableInfo{ + Name: table.Name, + CatalogName: table.CatalogName, + SchemaName: table.SchemaName, + FullName: fmt.Sprintf("%s.%s.%s", table.CatalogName, table.SchemaName, table.Name), + TableType: string(table.TableType), + Owner: owner, + Comment: comment, + } + } + + return &ListTablesResult{Tables: infos}, nil +} diff --git a/experimental/apps-mcp/lib/providers/databricks/deployment.go b/experimental/apps-mcp/lib/providers/databricks/deployment.go new file mode 100644 index 0000000000..11df0bf0e0 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/databricks/deployment.go @@ -0,0 +1,120 @@ +package databricks + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "time" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/databricks-sdk-go/service/apps" + "github.com/databricks/databricks-sdk-go/service/iam" +) + +func GetSourcePath(app *apps.App) string { + if app.DefaultSourceCodePath == "" { + return fmt.Sprintf("/Workspace/Users/%s/%s/", app.Creator, app.Name) + } + return app.DefaultSourceCodePath +} + +func GetAppInfo(ctx context.Context, cfg *mcp.Config, name string) (*apps.App, error) { + w := cmdctx.WorkspaceClient(ctx) + app, err := w.Apps.GetByName(ctx, name) + if err != nil { + return nil, fmt.Errorf("failed to get app info: %w", err) + } + + return app, nil +} + +func CreateApp(ctx context.Context, cfg *mcp.Config, createAppRequest *apps.CreateAppRequest) (*apps.App, error) { + w := cmdctx.WorkspaceClient(ctx) + + wait, err := w.Apps.Create(ctx, *createAppRequest) + if err != nil { + return nil, fmt.Errorf("failed to create app: %w", err) + } + + createdApp, err := wait.GetWithTimeout(5 * time.Minute) + if err != nil { + return nil, fmt.Errorf("failed to wait for app creation: %w", err) + } + + return createdApp, nil +} + +func GetUserInfo(ctx context.Context, cfg *mcp.Config) (*iam.User, error) { + w := cmdctx.WorkspaceClient(ctx) + user, err := w.CurrentUser.Me(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get user info: %w", err) + } + + return user, nil +} + +func SyncWorkspace(appInfo *apps.App, sourceDir string) error { + targetPath := GetSourcePath(appInfo) + + cmd := exec.Command( + "databricks", + "sync", + "--include", "public", + "--exclude", "node_modules", + ".", + targetPath, + ) + cmd.Dir = sourceDir + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to sync workspace: %w (output: %s)", err, string(output)) + } + + return nil +} + +func DeployApp(ctx context.Context, cfg *mcp.Config, appInfo *apps.App) error { + w := cmdctx.WorkspaceClient(ctx) + sourcePath := GetSourcePath(appInfo) + + req := apps.CreateAppDeploymentRequest{ + AppName: appInfo.Name, + AppDeployment: apps.AppDeployment{ + SourceCodePath: sourcePath, + Mode: apps.AppDeploymentModeSnapshot, + }, + } + + wait, err := w.Apps.Deploy(ctx, req) + if err != nil { + return fmt.Errorf("failed to deploy app: %w", err) + } + + _, err = wait.GetWithTimeout(10 * time.Minute) + if err != nil { + return fmt.Errorf("failed to wait for app deployment: %w", err) + } + + return nil +} + +func ResourcesFromEnv() (*apps.AppResource, error) { + warehouseID := os.Getenv("DATABRICKS_WAREHOUSE_ID") + if warehouseID == "" { + return nil, errors.New("DATABRICKS_WAREHOUSE_ID environment variable is required for app deployment. Set this to your Databricks SQL warehouse ID") + } + + return &apps.AppResource{ + Name: "base", + Description: "template resources", + SqlWarehouse: &apps.AppResourceSqlWarehouse{ + Id: warehouseID, + Permission: apps.AppResourceSqlWarehouseSqlWarehousePermissionCanUse, + }, + }, nil +} diff --git a/experimental/apps-mcp/lib/providers/databricks/format.go b/experimental/apps-mcp/lib/providers/databricks/format.go new file mode 100644 index 0000000000..531fcdf208 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/databricks/format.go @@ -0,0 +1,178 @@ +package databricks + +import ( + "fmt" + "strings" +) + +// formatCatalogsResult formats the catalogs result into a readable string +func formatCatalogsResult(result *ListCatalogsResult) string { + if len(result.Catalogs) == 0 { + return "No catalogs found." + } + + lines := []string{fmt.Sprintf("Found %d catalogs:", len(result.Catalogs)), ""} + for _, catalog := range result.Catalogs { + lines = append(lines, "• "+catalog) + } + return strings.Join(lines, "\n") +} + +// formatSchemasResult formats the schemas result into a readable string +func formatSchemasResult(result *ListSchemasResult) string { + var paginationInfo string + if result.TotalCount > result.Limit+result.Offset { + paginationInfo = fmt.Sprintf("Showing %d items (offset %d, limit %d). Total: %d", + result.ShownCount, result.Offset, result.Limit, result.TotalCount) + } else if result.Offset > 0 { + paginationInfo = fmt.Sprintf("Showing %d items (offset %d). Total: %d", + result.ShownCount, result.Offset, result.TotalCount) + } else if result.TotalCount > result.Limit { + paginationInfo = fmt.Sprintf("Showing %d items (limit %d). Total: %d", + result.ShownCount, result.Limit, result.TotalCount) + } else { + paginationInfo = fmt.Sprintf("Showing all %d items", result.TotalCount) + } + + if len(result.Schemas) == 0 { + return paginationInfo + } + + lines := []string{paginationInfo, ""} + for _, schema := range result.Schemas { + lines = append(lines, "• "+schema) + } + return strings.Join(lines, "\n") +} + +// formatTablesResult formats the tables result into a readable string +func formatTablesResult(result *ListTablesResult) string { + if len(result.Tables) == 0 { + return "No tables found." + } + + lines := []string{fmt.Sprintf("Found %d tables:", len(result.Tables)), ""} + for _, table := range result.Tables { + info := fmt.Sprintf("• %s (%s)", table.FullName, table.TableType) + if table.Owner != nil { + info += " - Owner: " + *table.Owner + } + if table.Comment != nil { + info += " - " + *table.Comment + } + lines = append(lines, info) + } + return strings.Join(lines, "\n") +} + +// formatTableDetails formats the table details into a readable string +func formatTableDetails(details *TableDetails) string { + var lines []string + + lines = append(lines, "Table: "+details.FullName) + lines = append(lines, "Table Type: "+details.TableType) + + if details.Owner != nil { + lines = append(lines, "Owner: "+*details.Owner) + } + + if details.Comment != nil { + lines = append(lines, "Comment: "+*details.Comment) + } + + if details.RowCount != nil { + lines = append(lines, fmt.Sprintf("Row Count: %d", *details.RowCount)) + } + + if details.StorageLocation != nil { + lines = append(lines, "Storage Location: "+*details.StorageLocation) + } + + if details.DataSourceFormat != nil { + lines = append(lines, "Data Source Format: "+*details.DataSourceFormat) + } + + if len(details.Columns) > 0 { + lines = append(lines, fmt.Sprintf("\nColumns (%d):", len(details.Columns))) + for _, col := range details.Columns { + colInfo := fmt.Sprintf(" - %s: %s", col.Name, col.DataType) + if col.Comment != nil { + colInfo += fmt.Sprintf(" (%s)", *col.Comment) + } + lines = append(lines, colInfo) + } + } + + if len(details.SampleData) > 0 { + lines = append(lines, fmt.Sprintf("\nSample Data (%d rows):", len(details.SampleData))) + sampleLimit := 5 + if len(details.SampleData) < sampleLimit { + sampleLimit = len(details.SampleData) + } + for i := range sampleLimit { + row := details.SampleData[i] + var rowParts []string + for k, v := range row { + rowParts = append(rowParts, fmt.Sprintf("%s: %s", k, formatValue(v))) + } + lines = append(lines, fmt.Sprintf(" Row %d: %s", i+1, strings.Join(rowParts, ", "))) + } + if len(details.SampleData) > 5 { + lines = append(lines, "...") + } + } + + return strings.Join(lines, "\n") +} + +func formatValue(v any) string { + if v == nil { + return "null" + } + return fmt.Sprintf("%v", v) +} + +// formatQueryResult formats query results into a readable string +func formatQueryResult(rows []map[string]any) string { + if len(rows) == 0 { + return "Query executed successfully but returned no results." + } + + var lines []string + lines = append(lines, fmt.Sprintf("Query returned %d rows:", len(rows))) + lines = append(lines, "") + + if len(rows) > 0 { + // Get column names from first row + var columns []string + for key := range rows[0] { + columns = append(columns, key) + } + + lines = append(lines, "Columns: "+strings.Join(columns, ", ")) + lines = append(lines, "") + lines = append(lines, "Results:") + + limit := 100 + if len(rows) < limit { + limit = len(rows) + } + + for i := range limit { + row := rows[i] + var rowParts []string + for _, col := range columns { + if val, ok := row[col]; ok { + rowParts = append(rowParts, fmt.Sprintf("%s: %s", col, formatValue(val))) + } + } + lines = append(lines, fmt.Sprintf(" Row %d: %s", i+1, strings.Join(rowParts, ", "))) + } + + if len(rows) > 100 { + lines = append(lines, fmt.Sprintf("\n... showing first 100 of %d total rows", len(rows))) + } + } + + return strings.Join(lines, "\n") +} diff --git a/experimental/apps-mcp/lib/providers/databricks/provider.go b/experimental/apps-mcp/lib/providers/databricks/provider.go new file mode 100644 index 0000000000..d7e5c47784 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/databricks/provider.go @@ -0,0 +1,203 @@ +package databricks + +import ( + "context" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + mcpsdk "github.com/databricks/cli/experimental/apps-mcp/lib/mcp" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers" + "github.com/databricks/cli/experimental/apps-mcp/lib/session" + "github.com/databricks/cli/libs/log" +) + +func init() { + providers.Register("databricks", func(ctx context.Context, cfg *mcp.Config, sess *session.Session) (providers.Provider, error) { + return NewProvider(ctx, cfg, sess) + }, providers.ProviderConfig{ + Always: true, + }) +} + +// Provider represents the Databricks provider that registers MCP tools +type Provider struct { + config *mcp.Config + session *session.Session + ctx context.Context +} + +// NewProvider creates a new Databricks provider +func NewProvider(ctx context.Context, cfg *mcp.Config, sess *session.Session) (*Provider, error) { + return &Provider{ + config: cfg, + session: sess, + ctx: ctx, + }, nil +} + +// Name returns the name of the provider. +func (p *Provider) Name() string { + return "databricks" +} + +// RegisterTools registers all Databricks tools with the MCP server +func (p *Provider) RegisterTools(server *mcpsdk.Server) error { + log.Info(p.ctx, "Registering Databricks tools") + + // Register databricks_list_catalogs + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "databricks_list_catalogs", + Description: "List all available Databricks catalogs", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args struct{}) (*mcpsdk.CallToolResult, any, error) { + log.Debug(ctx, "databricks_list_catalogs called") + + result, err := ListCatalogs(ctx, p.config) + if err != nil { + return nil, nil, err + } + + text := formatCatalogsResult(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + }, nil, nil + }), + ) + + // Register databricks_list_schemas + type ListSchemasInput struct { + CatalogName string `json:"catalog_name" jsonschema:"required" jsonschema_description:"Name of the catalog"` + Filter string `json:"filter,omitempty" jsonschema_description:"Optional filter string to search schema names"` + Limit int `json:"limit,omitempty" jsonschema_description:"Maximum number of schemas to return (default: 1000)"` + Offset int `json:"offset,omitempty" jsonschema_description:"Offset for pagination (default: 0)"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "databricks_list_schemas", + Description: "List all schemas in a Databricks catalog with pagination support", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args ListSchemasInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "databricks_list_schemas called: catalog=%s", args.CatalogName) + + listArgs := &ListSchemasArgs{ + CatalogName: args.CatalogName, + Filter: args.Filter, + Limit: args.Limit, + Offset: args.Offset, + } + + result, err := ListSchemas(ctx, p.config, listArgs) + if err != nil { + return nil, nil, err + } + + text := formatSchemasResult(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + }, nil, nil + }), + ) + + // Register databricks_list_tables + type ListTablesInput struct { + CatalogName string `json:"catalog_name" jsonschema:"required" jsonschema_description:"Name of the catalog"` + SchemaName string `json:"schema_name" jsonschema:"required" jsonschema_description:"Name of the schema"` + ExcludeInaccessible bool `json:"exclude_inaccessible,omitempty" jsonschema_description:"Exclude inaccessible tables (default: false)"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "databricks_list_tables", + Description: "List tables in a Databricks catalog and schema", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args ListTablesInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "databricks_list_tables called: catalog=%s, schema=%s", args.CatalogName, args.SchemaName) + + listArgs := &ListTablesArgs{ + CatalogName: args.CatalogName, + SchemaName: args.SchemaName, + ExcludeInaccessible: args.ExcludeInaccessible, + } + + result, err := ListTables(ctx, p.config, listArgs) + if err != nil { + return nil, nil, err + } + + text := formatTablesResult(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + }, nil, nil + }), + ) + + // Register databricks_describe_table + type DescribeTableInput struct { + TableFullName string `json:"table_full_name" jsonschema:"required" jsonschema_description:"Full name of the table (catalog.schema.table)"` + SampleSize int `json:"sample_size,omitempty" jsonschema_description:"Number of sample rows to return (default: 5)"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "databricks_describe_table", + Description: "Get detailed information about a Databricks table including schema and optional sample data", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args DescribeTableInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "databricks_describe_table called: table=%s", args.TableFullName) + + descArgs := &DescribeTableArgs{ + TableFullName: args.TableFullName, + SampleSize: args.SampleSize, + } + + result, err := DescribeTable(ctx, p.config, descArgs) + if err != nil { + return nil, nil, err + } + + text := formatTableDetails(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + }, nil, nil + }), + ) + + // Register databricks_execute_query + type ExecuteQueryInput struct { + Query string `json:"query" jsonschema:"required" jsonschema_description:"SQL query to execute"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "databricks_execute_query", + Description: "Execute SQL query in Databricks. Only single SQL statements are supported - do not send multiple statements separated by semicolons. For multiple statements, call this tool separately for each one. DO NOT create catalogs, schemas or tables - requires metastore admin privileges. Query existing data instead. Timeout: 60 seconds for query execution.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args ExecuteQueryInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "databricks_execute_query called: query=%s", args.Query) + + result, err := ExecuteQuery(ctx, p.config, args.Query) + if err != nil { + return nil, nil, err + } + + text := formatQueryResult(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + }, nil, nil + }), + ) + + log.Info(p.ctx, "Registered Databricks tools") + return nil +} diff --git a/experimental/apps-mcp/lib/providers/databricks/sql.go b/experimental/apps-mcp/lib/providers/databricks/sql.go new file mode 100644 index 0000000000..ef1c74d187 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/databricks/sql.go @@ -0,0 +1,71 @@ +package databricks + +import ( + "context" + "errors" + "fmt" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/databricks-sdk-go/service/sql" +) + +// ExecuteQueryArgs represents arguments for executing a SQL query +type ExecuteQueryArgs struct { + Query string `json:"query"` +} + +// ExecuteQuery executes a SQL query and returns the results +func ExecuteQuery(ctx context.Context, cfg *mcp.Config, query string) ([]map[string]any, error) { + // Get warehouse ID from config + if cfg.WarehouseID == "" { + return nil, errors.New("DATABRICKS_WAREHOUSE_ID not configured") + } + + w := cmdctx.WorkspaceClient(ctx) + + // Execute statement + result, err := w.StatementExecution.ExecuteStatement(ctx, sql.ExecuteStatementRequest{ + Statement: query, + WarehouseId: cfg.WarehouseID, + WaitTimeout: "30s", + Format: sql.FormatJsonArray, + }) + if err != nil { + return nil, fmt.Errorf("failed to execute query: %w", err) + } + + // Check status + if result.Status.State == sql.StatementStateFailed { + errMsg := "unknown error" + if result.Status.Error != nil && result.Status.Error.Message != "" { + errMsg = result.Status.Error.Message + } + return nil, fmt.Errorf("query failed: %s", errMsg) + } + + // Parse results + if result.Result == nil || result.Result.DataArray == nil { + return []map[string]any{}, nil + } + + // Get column names + columns := make([]string, len(result.Manifest.Schema.Columns)) + for i, col := range result.Manifest.Schema.Columns { + columns[i] = col.Name + } + + // Convert data array to map + rows := make([]map[string]any, len(result.Result.DataArray)) + for i, row := range result.Result.DataArray { + rowMap := make(map[string]any) + for j, val := range row { + if j < len(columns) { + rowMap[columns[j]] = val + } + } + rows[i] = rowMap + } + + return rows, nil +} diff --git a/experimental/apps-mcp/lib/providers/databricks/tables.go b/experimental/apps-mcp/lib/providers/databricks/tables.go new file mode 100644 index 0000000000..d926650055 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/databricks/tables.go @@ -0,0 +1,115 @@ +package databricks + +import ( + "context" + "fmt" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// DescribeTableArgs represents arguments for describing a table +type DescribeTableArgs struct { + TableFullName string `json:"table_full_name"` + SampleSize int `json:"sample_size,omitempty"` +} + +// ColumnMetadata represents metadata about a table column +type ColumnMetadata struct { + Name string `json:"name"` + DataType string `json:"data_type"` + Comment *string `json:"comment,omitempty"` +} + +// TableDetails represents detailed information about a table +type TableDetails struct { + FullName string `json:"full_name"` + TableType string `json:"table_type"` + Owner *string `json:"owner,omitempty"` + Comment *string `json:"comment,omitempty"` + StorageLocation *string `json:"storage_location,omitempty"` + DataSourceFormat *string `json:"data_source_format,omitempty"` + Columns []ColumnMetadata `json:"columns"` + SampleData []map[string]any `json:"sample_data,omitempty"` + RowCount *int64 `json:"row_count,omitempty"` +} + +// DescribeTable retrieves detailed information about a table including metadata and sample data +func DescribeTable(ctx context.Context, cfg *mcp.Config, args *DescribeTableArgs) (*TableDetails, error) { + if args.SampleSize == 0 { + args.SampleSize = 5 + } + + w := cmdctx.WorkspaceClient(ctx) + + // Get table metadata + tableInfo, err := w.Tables.Get(ctx, catalog.GetTableRequest{ + FullName: args.TableFullName, + }) + if err != nil { + return nil, fmt.Errorf("failed to get table: %w", err) + } + + // Build column metadata + columns := make([]ColumnMetadata, len(tableInfo.Columns)) + for i, col := range tableInfo.Columns { + var comment *string + if col.Comment != "" { + comment = &col.Comment + } + columns[i] = ColumnMetadata{ + Name: col.Name, + DataType: string(col.TypeName), + Comment: comment, + } + } + + var owner, tableComment, storageLocation, dataSourceFormat *string + if tableInfo.Owner != "" { + owner = &tableInfo.Owner + } + if tableInfo.Comment != "" { + tableComment = &tableInfo.Comment + } + if tableInfo.StorageLocation != "" { + storageLocation = &tableInfo.StorageLocation + } + if tableInfo.DataSourceFormat != "" { + dsf := string(tableInfo.DataSourceFormat) + dataSourceFormat = &dsf + } + + details := &TableDetails{ + FullName: args.TableFullName, + TableType: string(tableInfo.TableType), + Owner: owner, + Comment: tableComment, + StorageLocation: storageLocation, + DataSourceFormat: dataSourceFormat, + Columns: columns, + } + + // Get sample data if requested + if args.SampleSize > 0 { + query := fmt.Sprintf("SELECT * FROM %s LIMIT %d", args.TableFullName, args.SampleSize) + sampleData, err := ExecuteQuery(ctx, cfg, query) + if err == nil && len(sampleData) > 0 { + details.SampleData = sampleData + } + // Note: We intentionally don't return error for sample data failures + // as the table metadata is still valuable + } + + // Get row count + countQuery := "SELECT COUNT(*) as count FROM " + args.TableFullName + countData, err := ExecuteQuery(ctx, cfg, countQuery) + if err == nil && len(countData) > 0 { + if count, ok := countData[0]["count"].(int64); ok { + details.RowCount = &count + } + } + // Note: We intentionally don't return error for row count failures + + return details, nil +} diff --git a/experimental/apps-mcp/lib/providers/deployment/provider.go b/experimental/apps-mcp/lib/providers/deployment/provider.go new file mode 100644 index 0000000000..60f05b3746 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/deployment/provider.go @@ -0,0 +1,338 @@ +package deployment + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + mcpsdk "github.com/databricks/cli/experimental/apps-mcp/lib/mcp" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers/databricks" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers/io" + "github.com/databricks/cli/experimental/apps-mcp/lib/session" + "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go/service/apps" +) + +func init() { + // Register deployment provider with conditional enablement based on AllowDeployment + providers.Register("deployment", func(ctx context.Context, cfg *mcp.Config, sess *session.Session) (providers.Provider, error) { + return NewProvider(ctx, cfg, sess) + }, providers.ProviderConfig{ + EnabledWhen: func(cfg *mcp.Config) bool { + return cfg.AllowDeployment + }, + }) +} + +const deployRetries = 3 + +// Provider implements Databricks app deployment functionality. +type Provider struct { + config *mcp.Config + session *session.Session + ctx context.Context +} + +// DeployDatabricksAppInput contains parameters for deploying a Databricks app. +type DeployDatabricksAppInput struct { + WorkDir string `json:"work_dir" jsonschema:"required" jsonschema_description:"Absolute path to the work directory containing the app to deploy"` + Name string `json:"name" jsonschema:"required" jsonschema_description:"Name of the Databricks app (alphanumeric and dash characters only)"` + Description string `json:"description" jsonschema:"required" jsonschema_description:"Description of the Databricks app"` + Force bool `json:"force,omitempty" jsonschema_description:"Force re-deployment if the app already exists"` +} + +func NewProvider(ctx context.Context, cfg *mcp.Config, sess *session.Session) (*Provider, error) { + return &Provider{ + config: cfg, + session: sess, + ctx: ctx, + }, nil +} + +// Name returns the name of the provider. +func (p *Provider) Name() string { + return "deployment" +} + +func (p *Provider) RegisterTools(server *mcpsdk.Server) error { + log.Info(p.ctx, "Registering deployment tools") + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "deploy_databricks_app", + Description: "Deploy a generated app to Databricks Apps. Creates the app if it doesn't exist, syncs local files to workspace, and deploys the app. Returns deployment status and app URL. Only use after direct user request and running validation.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args DeployDatabricksAppInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "deploy_databricks_app called: work_dir=%s, name=%s, force=%v", + args.WorkDir, args.Name, args.Force) + + if !filepath.IsAbs(args.WorkDir) { + return nil, nil, fmt.Errorf("work_dir must be an absolute path, got: '%s'. Relative paths are not supported", args.WorkDir) + } + + result, err := p.deployDatabricksApp(ctx, &args) + if err != nil { + return nil, nil, err + } + + if !result.Success { + return nil, nil, fmt.Errorf("%s", result.Message) + } + + text := formatDeployResult(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + }, nil, nil + }), + ) + + return nil +} + +// DeployResult contains the outcome of a Databricks app deployment. +type DeployResult struct { + Success bool + Message string + AppURL string + AppName string +} + +func (p *Provider) deployDatabricksApp(ctx context.Context, args *DeployDatabricksAppInput) (*DeployResult, error) { + startTime := time.Now() + + workPath := args.WorkDir + if _, err := os.Stat(workPath); os.IsNotExist(err) { + return &DeployResult{ + Success: false, + Message: "Work directory does not exist: " + workPath, + AppName: args.Name, + }, nil + } + + fileInfo, err := os.Stat(workPath) + if err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to stat work directory: %v", err), + AppName: args.Name, + }, nil + } + + if !fileInfo.IsDir() { + return &DeployResult{ + Success: false, + Message: "Work path is not a directory: " + workPath, + AppName: args.Name, + }, nil + } + + projectState, err := io.LoadState(workPath) + if err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to load project state: %v", err), + AppName: args.Name, + }, nil + } + + if projectState == nil { + return &DeployResult{ + Success: false, + Message: "Project must be scaffolded before deployment", + AppName: args.Name, + }, nil + } + + expectedChecksum, hasChecksum := projectState.Checksum() + if !hasChecksum { + return &DeployResult{ + Success: false, + Message: "Project must be validated before deployment. Run validate_data_app first.", + AppName: args.Name, + }, nil + } + + checksumValid, err := io.VerifyChecksum(workPath, expectedChecksum) + if err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to verify project checksum: %v", err), + AppName: args.Name, + }, nil + } + + if !checksumValid { + return &DeployResult{ + Success: false, + Message: "Project files changed since validation. Re-run validate_data_app before deployment.", + AppName: args.Name, + }, nil + } + + log.Infof(ctx, "Installing dependencies: work_dir=%s", workPath) + if err := p.runCommand(workPath, "npm", "install"); err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to install dependencies: %v", err), + AppName: args.Name, + }, nil + } + + log.Infof(ctx, "Building frontend: work_dir=%s", workPath) + if err := p.runCommand(workPath, "npm", "run", "build"); err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to build frontend: %v", err), + AppName: args.Name, + }, nil + } + + appInfo, err := p.getOrCreateApp(ctx, args.Name, args.Description, args.Force) + if err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to get or create app: %v", err), + AppName: args.Name, + }, nil + } + + serverDir := filepath.Join(workPath, "server") + syncStart := time.Now() + log.Infof(ctx, "Syncing workspace: source=%s, target=%s", serverDir, databricks.GetSourcePath(appInfo)) + + if err := databricks.SyncWorkspace(appInfo, serverDir); err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to sync workspace: %v", err), + AppName: args.Name, + }, nil + } + + log.Infof(ctx, "Workspace sync completed: duration_seconds=%.2f", time.Since(syncStart).Seconds()) + + deployStart := time.Now() + log.Infof(ctx, "Deploying app: name=%s", args.Name) + + var deployErr error + for attempt := 1; attempt <= deployRetries; attempt++ { + deployErr = databricks.DeployApp(ctx, p.config, appInfo) + if deployErr == nil { + break + } + + if attempt < deployRetries { + log.Warnf(ctx, "Deploy attempt failed, retrying: attempt=%d, error=%s", + attempt, deployErr.Error()) + } + } + + if deployErr != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to deploy app after %d attempts: %v", deployRetries, deployErr), + AppName: args.Name, + }, nil + } + + log.Infof(ctx, "App deployment completed: duration_seconds=%.2f", time.Since(deployStart).Seconds()) + + deployedState, err := projectState.Deploy() + if err != nil { + return &DeployResult{ + Success: false, + Message: fmt.Sprintf("Failed to transition state: %v", err), + AppName: args.Name, + }, nil + } + + if err := io.SaveState(workPath, deployedState); err != nil { + log.Warnf(ctx, "Failed to save deployed state: error=%v", err) + } + + totalDuration := time.Since(startTime) + log.Infof(ctx, "Full deployment completed: duration_seconds=%.2f, app_url=%s", + totalDuration.Seconds(), appInfo.Url) + + return &DeployResult{ + Success: true, + Message: "Deployment completed successfully", + AppURL: appInfo.Url, + AppName: args.Name, + }, nil +} + +func (p *Provider) getOrCreateApp(ctx context.Context, name, description string, force bool) (*apps.App, error) { + appInfo, err := databricks.GetAppInfo(ctx, p.config, name) + if err == nil { + log.Infof(ctx, "Found existing app: name=%s", name) + + if !force { + userInfo, err := databricks.GetUserInfo(ctx, p.config) + if err != nil { + return nil, fmt.Errorf("failed to get user info: %w", err) + } + + if appInfo.Creator != userInfo.UserName { + return nil, fmt.Errorf( + "app '%s' already exists and was created by another user: %s. Use 'force' option to override", + name, + appInfo.Creator, + ) + } + } + + return appInfo, nil + } + + log.Infof(ctx, "App not found, creating new app: name=%s", name) + + resources, err := databricks.ResourcesFromEnv() + if err != nil { + return nil, err + } + + createApp := &apps.CreateAppRequest{ + App: apps.App{ + Name: name, + Description: description, + Resources: []apps.AppResource{*resources}, + }, + } + + return databricks.CreateApp(ctx, p.config, createApp) +} + +func (p *Provider) runCommand(dir, name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Dir = dir + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s failed: %w (output: %s)", name, err, string(output)) + } + + return nil +} + +func formatDeployResult(result *DeployResult) string { + if result.Success { + return fmt.Sprintf( + "Successfully deployed app '%s'\nURL: %s\n%s", + result.AppName, + result.AppURL, + result.Message, + ) + } + return fmt.Sprintf( + "Deployment failed for app '%s': %s", + result.AppName, + result.Message, + ) +} diff --git a/experimental/apps-mcp/lib/providers/doc.go b/experimental/apps-mcp/lib/providers/doc.go new file mode 100644 index 0000000000..ea4be0acbb --- /dev/null +++ b/experimental/apps-mcp/lib/providers/doc.go @@ -0,0 +1,20 @@ +/* +Package providers contains MCP tool providers. + +Each provider implements a set of related tools: + +- databricks: Databricks API integration +- io: Project scaffolding and validation +- workspace: File and command operations +- deployment: Application deployment (optional) + +Provider Interface: + + type Provider interface { + RegisterTools(server *mcp.Server) error + } + +Providers are registered with the MCP server during initialization +and their tools become available to AI agents. +*/ +package providers diff --git a/experimental/apps-mcp/lib/providers/io/format.go b/experimental/apps-mcp/lib/providers/io/format.go new file mode 100644 index 0000000000..cc9a1c1c45 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/io/format.go @@ -0,0 +1,37 @@ +package io + +import "fmt" + +// formatScaffoldResult formats a ScaffoldResult for display +func formatScaffoldResult(result *ScaffoldResult) string { + return fmt.Sprintf( + "Successfully scaffolded %s template to %s\n\n"+ + "Files copied: %d\n\n"+ + "Template: %s\n\n"+ + "%s", + result.TemplateName, + result.WorkDir, + result.FilesCopied, + result.TemplateName, + result.TemplateDescription, + ) +} + +// formatValidateResult formats a ValidateResult for display +func formatValidateResult(result *ValidateResult) string { + if result.Success { + return "✓ " + result.Message + } + + if result.Details == nil { + return "✗ " + result.Message + } + + return fmt.Sprintf( + "✗ %s\n\nExit code: %d\n\nStdout:\n%s\n\nStderr:\n%s", + result.Message, + result.Details.ExitCode, + result.Details.Stdout, + result.Details.Stderr, + ) +} diff --git a/experimental/apps-mcp/lib/providers/io/provider.go b/experimental/apps-mcp/lib/providers/io/provider.go new file mode 100644 index 0000000000..f68aedd3df --- /dev/null +++ b/experimental/apps-mcp/lib/providers/io/provider.go @@ -0,0 +1,123 @@ +package io + +import ( + "context" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + mcpsdk "github.com/databricks/cli/experimental/apps-mcp/lib/mcp" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers" + "github.com/databricks/cli/experimental/apps-mcp/lib/session" + "github.com/databricks/cli/experimental/apps-mcp/lib/templates" + "github.com/databricks/cli/libs/log" +) + +func init() { + providers.Register("io", func(ctx context.Context, cfg *mcp.Config, sess *session.Session) (providers.Provider, error) { + return NewProvider(ctx, cfg.IoConfig, sess) + }, providers.ProviderConfig{ + Always: true, + }) +} + +// Provider implements the I/O provider for scaffolding and validation +type Provider struct { + config *mcp.IoConfig + session *session.Session + ctx context.Context + defaultTemplate templates.Template +} + +// NewProvider creates a new I/O provider +func NewProvider(ctx context.Context, cfg *mcp.IoConfig, sess *session.Session) (*Provider, error) { + return &Provider{ + config: cfg, + session: sess, + ctx: ctx, + defaultTemplate: templates.GetTRPCTemplate(), + }, nil +} + +// Name returns the name of the provider. +func (p *Provider) Name() string { + return "io" +} + +// RegisterTools registers all I/O tools with the MCP server +func (p *Provider) RegisterTools(server *mcpsdk.Server) error { + log.Info(p.ctx, "Registering I/O tools") + + // Register scaffold_data_app + type ScaffoldInput struct { + WorkDir string `json:"work_dir" jsonschema:"required" jsonschema_description:"Absolute path to the work directory"` + ForceRewrite bool `json:"force_rewrite,omitempty" jsonschema_description:"Overwrite existing files if directory is not empty"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "scaffold_data_app", + Description: "Initialize a project by copying template files from the default TypeScript (tRPC + React) template to a work directory. Supports force rewrite to wipe and recreate the directory. It sets up a basic project structure, and should be ALWAYS used as the first step in creating a new data or web app.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args ScaffoldInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "scaffold_data_app called: work_dir=%s", args.WorkDir) + + scaffoldArgs := &ScaffoldArgs{ + WorkDir: args.WorkDir, + ForceRewrite: args.ForceRewrite, + } + + result, err := p.Scaffold(ctx, scaffoldArgs) + if err != nil { + return nil, nil, err + } + + // Set work directory in session for workspace tools + if err := session.SetWorkDir(ctx, result.WorkDir); err != nil { + log.Warnf(ctx, "Failed to set work directory in session: error=%v", err) + } else { + log.Infof(ctx, "Work directory set in session: work_dir=%s", result.WorkDir) + } + + text := formatScaffoldResult(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + }, nil, nil + }), + ) + + // Register validate_data_app + type ValidateInput struct { + WorkDir string `json:"work_dir" jsonschema:"required" jsonschema_description:"Absolute path to the work directory"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "validate_data_app", + Description: "Validate a project by copying files to a sandbox and running validation checks. Project should be scaffolded first. Returns validation result with success status and details.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args ValidateInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "validate_data_app called: work_dir=%s", args.WorkDir) + + validateArgs := &ValidateArgs{ + WorkDir: args.WorkDir, + } + + result, err := p.Validate(ctx, validateArgs) + if err != nil { + return nil, nil, err + } + + text := formatValidateResult(result) + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: text}, + }, + IsError: !result.Success, + }, nil, nil + }), + ) + + log.Infof(p.ctx, "Registered I/O tools: count=%d", 2) + return nil +} diff --git a/experimental/apps-mcp/lib/providers/io/scaffold.go b/experimental/apps-mcp/lib/providers/io/scaffold.go new file mode 100644 index 0000000000..d1da6260e6 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/io/scaffold.go @@ -0,0 +1,107 @@ +package io + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/databricks/cli/experimental/apps-mcp/lib/templates" + "github.com/databricks/cli/libs/log" +) + +// ScaffoldArgs contains arguments for scaffolding operation +type ScaffoldArgs struct { + WorkDir string `json:"work_dir"` + ForceRewrite bool `json:"force_rewrite,omitempty"` +} + +// ScaffoldResult contains the result of a scaffold operation +type ScaffoldResult struct { + FilesCopied int `json:"files_copied"` + WorkDir string `json:"work_dir"` + TemplateName string `json:"template_name"` + TemplateDescription string `json:"template_description"` +} + +// Scaffold copies template files to the work directory +func (p *Provider) Scaffold(ctx context.Context, args *ScaffoldArgs) (*ScaffoldResult, error) { + // Validate work directory + workDir, err := filepath.Abs(args.WorkDir) + if err != nil { + return nil, fmt.Errorf("invalid work directory: %w", err) + } + + // Check if directory exists + if stat, err := os.Stat(workDir); err == nil { + if !stat.IsDir() { + return nil, errors.New("work_dir exists but is not a directory") + } + + // Check if empty + entries, err := os.ReadDir(workDir) + if err != nil { + return nil, err + } + + if len(entries) > 0 && !args.ForceRewrite { + return nil, errors.New("work_dir is not empty (use force_rewrite to overwrite)") + } + + // Clear directory if force_rewrite + if args.ForceRewrite { + if err := os.RemoveAll(workDir); err != nil { + return nil, fmt.Errorf("failed to clear directory: %w", err) + } + } + } + + // Create directory + if err := os.MkdirAll(workDir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create directory: %w", err) + } + + // Get template + template := p.getTemplate() + files, err := template.Files() + if err != nil { + return nil, fmt.Errorf("failed to read template: %w", err) + } + + // Copy files + filesCopied := 0 + for path, content := range files { + targetPath := filepath.Join(workDir, path) + + // Create parent directories + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { + return nil, fmt.Errorf("failed to create directory for %s: %w", path, err) + } + + // Write file + if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil { + return nil, fmt.Errorf("failed to write %s: %w", path, err) + } + + filesCopied++ + } + + log.Infof(ctx, "scaffolded project (template=%s, work_dir=%s, files=%d)", + template.Name(), workDir, filesCopied) + + return &ScaffoldResult{ + FilesCopied: filesCopied, + WorkDir: workDir, + TemplateName: template.Name(), + TemplateDescription: template.Description(), + }, nil +} + +func (p *Provider) getTemplate() templates.Template { + // TODO: Support custom templates by checking p.config.Template.Path + // and loading from filesystem. Not yet implemented. + + // Default to TRPC template + return p.defaultTemplate +} diff --git a/experimental/apps-mcp/lib/providers/io/state.go b/experimental/apps-mcp/lib/providers/io/state.go new file mode 100644 index 0000000000..3a7b767cef --- /dev/null +++ b/experimental/apps-mcp/lib/providers/io/state.go @@ -0,0 +1,320 @@ +package io + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "time" + + "github.com/databricks/cli/experimental/apps-mcp/lib/fileutil" +) + +const StateFileName = ".edda_state" + +// StateType represents the lifecycle state of a scaffolded project. +type StateType string + +const ( + StateScaffolded StateType = "Scaffolded" + StateValidated StateType = "Validated" + StateDeployed StateType = "Deployed" +) + +// String returns the string representation of the state. +func (s StateType) String() string { + return string(s) +} + +// IsValid checks if the state type is a valid value. +func (s StateType) IsValid() bool { + switch s { + case StateScaffolded, StateValidated, StateDeployed: + return true + default: + return false + } +} + +// ValidatedData contains metadata for a validated project state. +type ValidatedData struct { + ValidatedAt time.Time `json:"validated_at"` + Checksum string `json:"checksum"` +} + +// DeployedData contains metadata for a deployed project state. +type DeployedData struct { + ValidatedAt time.Time `json:"validated_at"` + Checksum string `json:"checksum"` + DeployedAt time.Time `json:"deployed_at"` +} + +// ProjectState tracks the current state and metadata of a scaffolded project. +type ProjectState struct { + State StateType `json:"state"` + Data any `json:"data,omitempty"` +} + +func NewProjectState() *ProjectState { + return &ProjectState{ + State: StateScaffolded, + } +} + +func (ps *ProjectState) Validate(checksum string) *ProjectState { + return &ProjectState{ + State: StateValidated, + Data: ValidatedData{ + ValidatedAt: time.Now().UTC(), + Checksum: checksum, + }, + } +} + +func (ps *ProjectState) extractValidatedData() (*ValidatedData, error) { + if data, ok := ps.Data.(ValidatedData); ok { + return &data, nil + } + + dataMap, ok := ps.Data.(map[string]any) + if !ok { + return nil, errors.New("invalid validated state data") + } + + validatedAtStr, ok := dataMap["validated_at"].(string) + if !ok { + return nil, errors.New("missing validated_at in state data") + } + + validatedAt, err := time.Parse(time.RFC3339, validatedAtStr) + if err != nil { + return nil, fmt.Errorf("invalid validated_at format: %w", err) + } + + checksum, ok := dataMap["checksum"].(string) + if !ok { + return nil, errors.New("missing checksum in state data") + } + + return &ValidatedData{ + ValidatedAt: validatedAt, + Checksum: checksum, + }, nil +} + +func (ps *ProjectState) extractChecksumFromMap() (string, bool) { + dataMap, ok := ps.Data.(map[string]any) + if !ok { + return "", false + } + checksum, ok := dataMap["checksum"].(string) + return checksum, ok +} + +func (ps *ProjectState) Deploy() (*ProjectState, error) { + if !ps.CanTransitionTo(StateDeployed) { + if ps.State == StateScaffolded { + return nil, errors.New("cannot deploy: project not validated") + } + if ps.State == StateDeployed { + return nil, errors.New("cannot deploy: project already deployed (re-validate first)") + } + return nil, fmt.Errorf("invalid state transition: %s -> Deployed", ps.State) + } + + data, err := ps.extractValidatedData() + if err != nil { + return nil, err + } + + return &ProjectState{ + State: StateDeployed, + Data: DeployedData{ + ValidatedAt: data.ValidatedAt, + Checksum: data.Checksum, + DeployedAt: time.Now().UTC(), + }, + }, nil +} + +func (ps *ProjectState) Checksum() (string, bool) { + switch ps.State { + case StateValidated: + if data, ok := ps.Data.(ValidatedData); ok { + return data.Checksum, true + } + return ps.extractChecksumFromMap() + case StateDeployed: + if data, ok := ps.Data.(DeployedData); ok { + return data.Checksum, true + } + return ps.extractChecksumFromMap() + case StateScaffolded: + return "", false + } + return "", false +} + +func (ps *ProjectState) IsValidated() bool { + return ps.State == StateValidated || ps.State == StateDeployed +} + +// CanTransitionTo checks if a state transition is valid according to the state machine rules. +// Valid transitions: +// - Scaffolded -> Validated +// - Validated -> Deployed (or re-validate to Validated) +// - Deployed -> Validated (re-validation allowed before re-deployment) +func (ps *ProjectState) CanTransitionTo(next StateType) bool { + switch ps.State { + case StateScaffolded: + return next == StateValidated + case StateValidated: + return next == StateDeployed || next == StateValidated + case StateDeployed: + return next == StateValidated + default: + return false + } +} + +// TransitionTo attempts to transition to a new state, returning an error if invalid. +func (ps *ProjectState) TransitionTo(next StateType) error { + if !next.IsValid() { + return fmt.Errorf("invalid target state: %s", next) + } + if !ps.CanTransitionTo(next) { + return fmt.Errorf("invalid state transition: %s -> %s", ps.State, next) + } + ps.State = next + return nil +} + +func LoadState(workDir string) (*ProjectState, error) { + statePath := filepath.Join(workDir, StateFileName) + + if _, err := os.Stat(statePath); os.IsNotExist(err) { + return nil, nil + } + + content, err := os.ReadFile(statePath) + if err != nil { + return nil, fmt.Errorf("failed to read state file: %w", err) + } + + var state ProjectState + if err := json.Unmarshal(content, &state); err != nil { + return nil, fmt.Errorf("failed to parse state file: %w", err) + } + + return &state, nil +} + +func SaveState(workDir string, state *ProjectState) error { + statePath := filepath.Join(workDir, StateFileName) + + content, err := json.MarshalIndent(state, "", " ") + if err != nil { + return fmt.Errorf("failed to serialize state: %w", err) + } + + if err := fileutil.AtomicWriteFile(statePath, content, 0o644); err != nil { + return fmt.Errorf("failed to write state file: %w", err) + } + + return nil +} + +func ComputeChecksum(workDir string) (string, error) { + var filesToHash []string + + for _, dir := range []string{"client", "server"} { + dirPath := filepath.Join(workDir, dir) + if info, err := os.Stat(dirPath); err == nil && info.IsDir() { + if err := collectSourceFiles(dirPath, &filesToHash); err != nil { + return "", err + } + } + } + + packageJSON := filepath.Join(workDir, "package.json") + if _, err := os.Stat(packageJSON); err == nil { + filesToHash = append(filesToHash, packageJSON) + } + + sort.Strings(filesToHash) + + if len(filesToHash) == 0 { + return "", errors.New("no files to hash - project structure appears invalid") + } + + hasher := sha256.New() + + for _, filePath := range filesToHash { + content, err := os.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", filePath, err) + } + _, _ = hasher.Write(content) + } + + return hex.EncodeToString(hasher.Sum(nil)), nil +} + +func VerifyChecksum(workDir, expected string) (bool, error) { + current, err := ComputeChecksum(workDir) + if err != nil { + return false, err + } + return current == expected, nil +} + +func collectSourceFiles(dir string, files *[]string) error { + entries, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("failed to read directory %s: %w", dir, err) + } + + excludedDirs := map[string]bool{ + "node_modules": true, + "dist": true, + ".git": true, + "build": true, + "coverage": true, + } + + validExtensions := map[string]bool{ + ".ts": true, + ".tsx": true, + ".js": true, + ".jsx": true, + ".json": true, + ".css": true, + ".html": true, + ".yaml": true, + ".yml": true, + } + + for _, entry := range entries { + path := filepath.Join(dir, entry.Name()) + + if entry.IsDir() { + if excludedDirs[entry.Name()] { + continue + } + if err := collectSourceFiles(path, files); err != nil { + return err + } + } else { + ext := filepath.Ext(path) + if validExtensions[ext] { + *files = append(*files, path) + } + } + } + + return nil +} diff --git a/experimental/apps-mcp/lib/providers/io/validate.go b/experimental/apps-mcp/lib/providers/io/validate.go new file mode 100644 index 0000000000..2a763ec968 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/io/validate.go @@ -0,0 +1,107 @@ +package io + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox/local" + "github.com/databricks/cli/libs/log" +) + +type ValidateArgs struct { + WorkDir string `json:"work_dir"` +} + +func (p *Provider) Validate(ctx context.Context, args *ValidateArgs) (*ValidateResult, error) { + workDir, err := filepath.Abs(args.WorkDir) + if err != nil { + return nil, fmt.Errorf("invalid work directory: %w", err) + } + + if !filepath.IsAbs(workDir) { + return nil, errors.New("work_dir must be an absolute path") + } + + if _, err := os.Stat(workDir); os.IsNotExist(err) { + return nil, errors.New("work directory does not exist") + } + + state, err := LoadState(workDir) + if err != nil { + log.Warnf(ctx, "failed to load project state: error=%v", err) + } + if state == nil { + state = NewProjectState() + } + + log.Infof(ctx, "starting validation: work_dir=%s, state=%s", workDir, string(state.State)) + + var validation Validation + if p.config != nil && p.config.Validation != nil { + valConfig := p.config.Validation + if valConfig.Command != "" { + log.Infof(ctx, "using custom validation command: command=%s", valConfig.Command) + validation = NewValidationCmd(valConfig.Command, "") + } + } + + if validation == nil { + log.Info(ctx, "using default tRPC validation strategy") + validation = NewValidationTRPC() + } + + log.Info(ctx, "using local sandbox for validation") + sb, err := p.createLocalSandbox(workDir) + if err != nil { + return nil, fmt.Errorf("failed to create local sandbox: %w", err) + } + sandboxType := "local" + + defer func() { + if closeErr := sb.Close(); closeErr != nil { + log.Warnf(ctx, "failed to close sandbox: error=%s", closeErr.Error()) + } + }() + + result, err := validation.Validate(ctx, sb) + if err != nil { + return nil, fmt.Errorf("validation execution failed: %w", err) + } + + if !result.Success { + log.Warnf(ctx, "validation failed: message=%s", result.Message) + return result, nil + } + + checksum, err := ComputeChecksum(workDir) + if err != nil { + log.Warnf(ctx, "failed to compute checksum: error=%s", err.Error()) + return &ValidateResult{ + Success: false, + Message: fmt.Sprintf("Validation passed but failed to compute checksum: %v", err), + }, nil + } + + validatedState := state.Validate(checksum) + if err := SaveState(workDir, validatedState); err != nil { + log.Warnf(ctx, "failed to save state: error=%s", err.Error()) + return &ValidateResult{ + Success: false, + Message: fmt.Sprintf("Validation passed but failed to save state: %v", err), + }, nil + } + + log.Infof(ctx, "validation successful: checksum=%s, state=%s, sandbox_type=%s", + checksum, string(validatedState.State), sandboxType) + + result.SandboxType = sandboxType + return result, nil +} + +func (p *Provider) createLocalSandbox(workDir string) (*local.LocalSandbox, error) { + log.Infof(p.ctx, "creating local sandbox: workDir=%s", workDir) + return local.NewLocalSandbox(workDir) +} diff --git a/experimental/apps-mcp/lib/providers/io/validation.go b/experimental/apps-mcp/lib/providers/io/validation.go new file mode 100644 index 0000000000..d67fd7d171 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/io/validation.go @@ -0,0 +1,279 @@ +package io + +import ( + "context" + "fmt" + "time" + + "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox" + "github.com/databricks/cli/libs/log" +) + +// ValidationDetail contains detailed output from a failed validation. +type ValidationDetail struct { + ExitCode int `json:"exit_code"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +func (vd *ValidationDetail) Error() string { + return fmt.Sprintf("validation failed (exit code %d)\nStdout:\n%s\nStderr:\n%s", + vd.ExitCode, vd.Stdout, vd.Stderr) +} + +// ValidateResult contains the outcome of a validation operation. +type ValidateResult struct { + Success bool `json:"success"` + Message string `json:"message"` + Details *ValidationDetail `json:"details,omitempty"` + ProgressLog []string `json:"progress_log,omitempty"` + SandboxType string `json:"sandbox_type,omitempty"` +} + +func (vr *ValidateResult) String() string { + var result string + + if len(vr.ProgressLog) > 0 { + result = "Validation Progress:\n" + for _, log := range vr.ProgressLog { + result += log + "\n" + } + result += "\n" + } + + if vr.Success { + result += "✓ " + vr.Message + } else { + result += "✗ " + vr.Message + if vr.Details != nil { + result += fmt.Sprintf("\n\nExit code: %d\n\nStdout:\n%s\n\nStderr:\n%s", + vr.Details.ExitCode, vr.Details.Stdout, vr.Details.Stderr) + } + } + + return result +} + +// Validation defines the interface for project validation strategies. +type Validation interface { + Validate(ctx context.Context, sb sandbox.Sandbox) (*ValidateResult, error) + DockerImage() string +} + +// ValidationTRPC implements validation for tRPC-based projects using build, type check, and tests. +type ValidationTRPC struct{} + +func NewValidationTRPC() Validation { + return &ValidationTRPC{} +} + +func (v *ValidationTRPC) DockerImage() string { + return "node:20-alpine3.22" +} + +func (v *ValidationTRPC) Validate(ctx context.Context, sb sandbox.Sandbox) (*ValidateResult, error) { + log.Info(ctx, "starting tRPC validation (build + type check + tests)") + startTime := time.Now() + var progressLog []string + + progressLog = append(progressLog, "🔄 Starting validation: build + type check + tests") + + log.Info(ctx, "step 1/3: running build...") + progressLog = append(progressLog, "⏳ Step 1/3: Running build...") + buildStart := time.Now() + if err := v.runBuild(ctx, sb); err != nil { + buildDuration := time.Since(buildStart) + log.Errorf(ctx, "build failed (duration: %.1fs)", buildDuration.Seconds()) + progressLog = append(progressLog, fmt.Sprintf("❌ Build failed (%.1fs)", buildDuration.Seconds())) + return &ValidateResult{ + Success: false, + Message: "Build failed", + Details: err, + ProgressLog: progressLog, + }, nil + } + buildDuration := time.Since(buildStart) + log.Infof(ctx, "✓ build passed: duration=%.1fs", buildDuration.Seconds()) + progressLog = append(progressLog, fmt.Sprintf("✅ Build passed (%.1fs)", buildDuration.Seconds())) + + log.Info(ctx, "step 2/3: running type check...") + progressLog = append(progressLog, "⏳ Step 2/3: Running type check...") + typeCheckStart := time.Now() + if err := v.runClientTypeCheck(ctx, sb); err != nil { + typeCheckDuration := time.Since(typeCheckStart) + log.Errorf(ctx, "type check failed (duration: %.1fs)", typeCheckDuration.Seconds()) + progressLog = append(progressLog, fmt.Sprintf("❌ Type check failed (%.1fs)", typeCheckDuration.Seconds())) + return &ValidateResult{ + Success: false, + Message: "Type check failed", + Details: err, + ProgressLog: progressLog, + }, nil + } + typeCheckDuration := time.Since(typeCheckStart) + log.Infof(ctx, "✓ type check passed: duration=%.1fs", typeCheckDuration.Seconds()) + progressLog = append(progressLog, fmt.Sprintf("✅ Type check passed (%.1fs)", typeCheckDuration.Seconds())) + + log.Info(ctx, "step 3/3: running tests...") + progressLog = append(progressLog, "⏳ Step 3/3: Running tests...") + testStart := time.Now() + if err := v.runTests(ctx, sb); err != nil { + testDuration := time.Since(testStart) + log.Errorf(ctx, "tests failed (duration: %.1fs)", testDuration.Seconds()) + progressLog = append(progressLog, fmt.Sprintf("❌ Tests failed (%.1fs)", testDuration.Seconds())) + return &ValidateResult{ + Success: false, + Message: "Tests failed", + Details: err, + ProgressLog: progressLog, + }, nil + } + testDuration := time.Since(testStart) + log.Infof(ctx, "✓ tests passed: duration=%.1fs", testDuration.Seconds()) + progressLog = append(progressLog, fmt.Sprintf("✅ Tests passed (%.1fs)", testDuration.Seconds())) + + totalDuration := time.Since(startTime) + log.Infof(ctx, "✓ all validation checks passed: total_duration=%.1fs, steps=%s", + totalDuration.Seconds(), "build + type check + tests") + progressLog = append(progressLog, fmt.Sprintf("✅ All checks passed! Total: %.1fs", totalDuration.Seconds())) + + return &ValidateResult{ + Success: true, + Message: "All validation checks passed (build + type check + tests)", + ProgressLog: progressLog, + }, nil +} + +func (v *ValidationTRPC) runBuild(ctx context.Context, sb sandbox.Sandbox) *ValidationDetail { + result, err := sb.Exec(ctx, "npm run build") + if err != nil { + return &ValidationDetail{ + ExitCode: -1, + Stdout: "", + Stderr: fmt.Sprintf("Failed to run npm build: %v", err), + } + } + + if result.ExitCode != 0 { + return &ValidationDetail{ + ExitCode: result.ExitCode, + Stdout: result.Stdout, + Stderr: result.Stderr, + } + } + + return nil +} + +func (v *ValidationTRPC) runClientTypeCheck(ctx context.Context, sb sandbox.Sandbox) *ValidationDetail { + result, err := sb.Exec(ctx, "cd client && npx tsc --noEmit") + if err != nil { + return &ValidationDetail{ + ExitCode: -1, + Stdout: "", + Stderr: fmt.Sprintf("Failed to run client type check: %v", err), + } + } + + if result.ExitCode != 0 { + return &ValidationDetail{ + ExitCode: result.ExitCode, + Stdout: result.Stdout, + Stderr: result.Stderr, + } + } + + return nil +} + +func (v *ValidationTRPC) runTests(ctx context.Context, sb sandbox.Sandbox) *ValidationDetail { + result, err := sb.Exec(ctx, "npm test") + if err != nil { + return &ValidationDetail{ + ExitCode: -1, + Stdout: "", + Stderr: fmt.Sprintf("Failed to run npm test: %v", err), + } + } + + if result.ExitCode != 0 { + return &ValidationDetail{ + ExitCode: result.ExitCode, + Stdout: result.Stdout, + Stderr: result.Stderr, + } + } + + return nil +} + +// ValidationCmd implements validation using a custom command specified by the user. +type ValidationCmd struct { + Command string + DockerImg string +} + +func NewValidationCmd(command, dockerImage string) Validation { + if dockerImage == "" { + dockerImage = "node:20-alpine3.22" + } + return &ValidationCmd{ + Command: command, + DockerImg: dockerImage, + } +} + +func (v *ValidationCmd) DockerImage() string { + return v.DockerImg +} + +func (v *ValidationCmd) Validate(ctx context.Context, sb sandbox.Sandbox) (*ValidateResult, error) { + log.Infof(ctx, "starting custom validation: command=%s", v.Command) + startTime := time.Now() + var progressLog []string + + progressLog = append(progressLog, "🔄 Starting custom validation: "+v.Command) + + fullCommand := v.Command + result, err := sb.Exec(ctx, fullCommand) + if err != nil { + duration := time.Since(startTime) + log.Errorf(ctx, "custom validation command failed (duration: %.1fs, error: %v)", duration.Seconds(), err) + progressLog = append(progressLog, fmt.Sprintf("❌ Command failed (%.1fs): %v", duration.Seconds(), err)) + return &ValidateResult{ + Success: false, + Message: "Custom validation command failed", + Details: &ValidationDetail{ + ExitCode: -1, + Stdout: "", + Stderr: fmt.Sprintf("Failed to run validation command: %v", err), + }, + ProgressLog: progressLog, + }, nil + } + + if result.ExitCode != 0 { + duration := time.Since(startTime) + log.Errorf(ctx, "custom validation failed (duration: %.1fs, exit_code: %d)", duration.Seconds(), result.ExitCode) + progressLog = append(progressLog, fmt.Sprintf("❌ Validation failed (%.1fs) - exit code: %d", duration.Seconds(), result.ExitCode)) + return &ValidateResult{ + Success: false, + Message: "Custom validation command failed", + Details: &ValidationDetail{ + ExitCode: result.ExitCode, + Stdout: result.Stdout, + Stderr: result.Stderr, + }, + ProgressLog: progressLog, + }, nil + } + + duration := time.Since(startTime) + log.Infof(ctx, "✓ custom validation passed: duration=%.1fs", duration.Seconds()) + progressLog = append(progressLog, fmt.Sprintf("✅ Custom validation passed (%.1fs)", duration.Seconds())) + return &ValidateResult{ + Success: true, + Message: "Custom validation passed", + ProgressLog: progressLog, + }, nil +} diff --git a/experimental/apps-mcp/lib/providers/registry.go b/experimental/apps-mcp/lib/providers/registry.go new file mode 100644 index 0000000000..aa5fc6b7d6 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/registry.go @@ -0,0 +1,135 @@ +// Package providers implements a registry pattern for automatic provider discovery and initialization. +package providers + +import ( + "context" + "fmt" + "sync" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + "github.com/databricks/cli/experimental/apps-mcp/lib/session" +) + +// Provider is the interface that all MCP providers must implement. +// Providers are responsible for registering their tools with the MCP server. +// Note: RegisterTools is not included in the interface due to type constraints, +// but providers should implement it with the appropriate server type. +type Provider interface { + // Name returns the unique name of the provider. + Name() string +} + +// ProviderFactory is a function that creates a new provider instance. +// It receives configuration, session, and logger instances. +type ProviderFactory func(ctx context.Context, cfg *mcp.Config, sess *session.Session) (Provider, error) + +// Registry manages provider registration and creation. +type Registry struct { + mu sync.RWMutex + factories map[string]ProviderFactory + config map[string]ProviderConfig +} + +// ProviderConfig holds configuration for conditional provider registration. +type ProviderConfig struct { + // Always indicates the provider should always be registered. + Always bool + // EnabledWhen is a function that determines if the provider should be enabled. + // If nil and Always is false, the provider won't be registered. + EnabledWhen func(*mcp.Config) bool +} + +var ( + globalRegistry *Registry + once sync.Once +) + +// GetRegistry returns the global provider registry singleton. +func GetRegistry() *Registry { + once.Do(func() { + globalRegistry = &Registry{ + factories: make(map[string]ProviderFactory), + config: make(map[string]ProviderConfig), + } + }) + return globalRegistry +} + +// Register registers a provider factory with the global registry. +// This is typically called from provider package init() functions. +func Register(name string, factory ProviderFactory, cfg ProviderConfig) { + GetRegistry().RegisterProvider(name, factory, cfg) +} + +// RegisterProvider registers a provider factory with this registry. +func (r *Registry) RegisterProvider(name string, factory ProviderFactory, cfg ProviderConfig) { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.factories[name]; exists { + panic(fmt.Sprintf("provider %q already registered", name)) + } + + r.factories[name] = factory + r.config[name] = cfg +} + +// Create creates a provider instance by name. +func (r *Registry) Create(ctx context.Context, name string, cfg *mcp.Config, sess *session.Session) (Provider, error) { + r.mu.RLock() + factory, exists := r.factories[name] + r.mu.RUnlock() + + if !exists { + return nil, fmt.Errorf("provider %q not registered", name) + } + + return factory(ctx, cfg, sess) +} + +// CreateAll creates all registered providers that are enabled according to their configuration. +func (r *Registry) CreateAll(ctx context.Context, cfg *mcp.Config, sess *session.Session) ([]Provider, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + var providers []Provider + + for name, factory := range r.factories { + providerCfg := r.config[name] + + shouldEnable := providerCfg.Always + if !shouldEnable && providerCfg.EnabledWhen != nil { + shouldEnable = providerCfg.EnabledWhen(cfg) + } + + if !shouldEnable { + continue + } + + provider, err := factory(ctx, cfg, sess) + if err != nil { + return nil, fmt.Errorf("failed to create provider %q: %w", name, err) + } + + providers = append(providers, provider) + } + + return providers, nil +} + +// List returns the names of all registered providers. +func (r *Registry) List() []string { + r.mu.RLock() + defer r.mu.RUnlock() + + names := make([]string, 0, len(r.factories)) + for name := range r.factories { + names = append(names, name) + } + return names +} + +// CreateAll is a convenience function that uses the global registry. +func CreateAll(ctx context.Context, cfg *mcp.Config, sess *session.Session) ([]Provider, error) { + return GetRegistry().CreateAll(ctx, cfg, sess) +} diff --git a/experimental/apps-mcp/lib/providers/workspace/bash.go b/experimental/apps-mcp/lib/providers/workspace/bash.go new file mode 100644 index 0000000000..01735d0f5a --- /dev/null +++ b/experimental/apps-mcp/lib/providers/workspace/bash.go @@ -0,0 +1,73 @@ +package workspace + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "time" +) + +// BashArgs contains arguments for bash execution +type BashArgs struct { + Command string `json:"command"` + Timeout int `json:"timeout,omitempty"` // Seconds, default 120 +} + +// BashResult contains the result of a bash command +type BashResult struct { + ExitCode int `json:"exit_code"` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// Bash executes a bash command in the workspace directory +func (p *Provider) Bash(ctx context.Context, args *BashArgs) (*BashResult, error) { + workDir, err := p.getWorkDir(ctx) + if err != nil { + return nil, err + } + + // Set timeout + timeout := time.Duration(args.Timeout) * time.Second + if args.Timeout == 0 { + timeout = 120 * time.Second + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + // Execute command + cmd := exec.CommandContext(ctx, "bash", "-c", args.Command) + cmd.Dir = workDir + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Run() + + // Check for timeout first + if ctx.Err() == context.DeadlineExceeded { + timeoutSecs := args.Timeout + if timeoutSecs == 0 { + timeoutSecs = 120 + } + return nil, fmt.Errorf("command timed out after %d seconds", timeoutSecs) + } + + exitCode := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } else { + return nil, fmt.Errorf("failed to execute command: %w", err) + } + } + + return &BashResult{ + ExitCode: exitCode, + Stdout: stdout.String(), + Stderr: stderr.String(), + }, nil +} diff --git a/experimental/apps-mcp/lib/providers/workspace/files.go b/experimental/apps-mcp/lib/providers/workspace/files.go new file mode 100644 index 0000000000..3aa69436d3 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/workspace/files.go @@ -0,0 +1,146 @@ +package workspace + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/databricks/cli/experimental/apps-mcp/lib/fileutil" +) + +// ReadFileArgs contains arguments for reading a file +type ReadFileArgs struct { + FilePath string `json:"file_path"` + Offset int `json:"offset,omitempty"` // Line number to start (1-indexed) + Limit int `json:"limit,omitempty"` // Number of lines to read +} + +// ReadFile reads a file from the workspace +func (p *Provider) ReadFile(ctx context.Context, args *ReadFileArgs) (string, error) { + workDir, err := p.getWorkDir(ctx) + if err != nil { + return "", err + } + + // Validate path + fullPath, err := validatePath(workDir, args.FilePath) + if err != nil { + return "", err + } + + // Read file + content, err := os.ReadFile(fullPath) + if err != nil { + return "", fmt.Errorf("failed to read file: %w", err) + } + + // Apply line offset and limit if specified + if args.Offset > 0 || args.Limit > 0 { + content = applyLineRange(content, args.Offset, args.Limit) + } + + return string(content), nil +} + +// WriteFileArgs contains arguments for writing a file +type WriteFileArgs struct { + FilePath string `json:"file_path"` + Content string `json:"content"` +} + +// WriteFile writes a file to the workspace +func (p *Provider) WriteFile(ctx context.Context, args *WriteFileArgs) error { + workDir, err := p.getWorkDir(ctx) + if err != nil { + return err + } + + // Validate path + fullPath, err := validatePath(workDir, args.FilePath) + if err != nil { + return err + } + + if err := fileutil.AtomicWriteFile(fullPath, []byte(args.Content), 0o644); err != nil { + return err + } + + return nil +} + +// EditFileArgs contains arguments for editing a file +type EditFileArgs struct { + FilePath string `json:"file_path"` + OldString string `json:"old_string"` + NewString string `json:"new_string"` +} + +// EditFile edits a file in the workspace by replacing old_string with new_string +func (p *Provider) EditFile(ctx context.Context, args *EditFileArgs) error { + workDir, err := p.getWorkDir(ctx) + if err != nil { + return err + } + + // Validate path + fullPath, err := validatePath(workDir, args.FilePath) + if err != nil { + return err + } + + // Read current content + content, err := os.ReadFile(fullPath) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + // Check if old_string exists + contentStr := string(content) + if !strings.Contains(contentStr, args.OldString) { + return errors.New("old_string not found in file") + } + + // Count occurrences + count := strings.Count(contentStr, args.OldString) + if count > 1 { + return fmt.Errorf("old_string appears %d times, must be unique", count) + } + + // Replace + newContent := strings.Replace(contentStr, args.OldString, args.NewString, 1) + + // Write back + if err := os.WriteFile(fullPath, []byte(newContent), 0o644); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil +} + +// applyLineRange applies line offset and limit to file content +func applyLineRange(content []byte, offset, limit int) []byte { + lines := bytes.Split(content, []byte("\n")) + + // Adjust for 1-indexed + if offset > 0 { + offset-- + } + + // Apply offset + if offset > 0 { + if offset >= len(lines) { + return []byte{} + } + lines = lines[offset:] + } + + // Apply limit + if limit > 0 && limit < len(lines) { + lines = lines[:limit] + } + + return bytes.Join(lines, []byte("\n")) +} diff --git a/experimental/apps-mcp/lib/providers/workspace/glob.go b/experimental/apps-mcp/lib/providers/workspace/glob.go new file mode 100644 index 0000000000..edab5b5151 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/workspace/glob.go @@ -0,0 +1,54 @@ +package workspace + +import ( + "context" + "fmt" + "path/filepath" + "sort" +) + +// GlobArgs contains arguments for glob operation +type GlobArgs struct { + Pattern string `json:"pattern"` +} + +// GlobResult contains the result of a glob operation +type GlobResult struct { + Files []string `json:"files"` + Total int `json:"total"` +} + +// Glob matches files against a pattern in the workspace +func (p *Provider) Glob(ctx context.Context, args *GlobArgs) (*GlobResult, error) { + workDir, err := p.getWorkDir(ctx) + if err != nil { + return nil, err + } + + // Resolve pattern relative to work dir + pattern := filepath.Join(workDir, args.Pattern) + + // Use filepath.Glob + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, fmt.Errorf("glob failed: %w", err) + } + + // Convert to relative paths + relMatches := make([]string, len(matches)) + for i, match := range matches { + relPath, err := filepath.Rel(workDir, match) + if err != nil { + relPath = match + } + relMatches[i] = relPath + } + + // Sort results + sort.Strings(relMatches) + + return &GlobResult{ + Files: relMatches, + Total: len(relMatches), + }, nil +} diff --git a/experimental/apps-mcp/lib/providers/workspace/grep.go b/experimental/apps-mcp/lib/providers/workspace/grep.go new file mode 100644 index 0000000000..7a67b980da --- /dev/null +++ b/experimental/apps-mcp/lib/providers/workspace/grep.go @@ -0,0 +1,131 @@ +package workspace + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +// GrepArgs contains arguments for grep operation +type GrepArgs struct { + Pattern string `json:"pattern"` + Path string `json:"path,omitempty"` // Limit to specific path + IgnoreCase bool `json:"ignore_case,omitempty"` + MaxResults int `json:"max_results,omitempty"` // Default 100 +} + +// GrepResult contains the result of a grep operation +type GrepResult struct { + Matches []GrepMatch `json:"matches"` + Total int `json:"total"` +} + +// GrepMatch represents a single grep match +type GrepMatch struct { + File string `json:"file"` + Line int `json:"line"` + Content string `json:"content"` +} + +// Grep searches for a pattern in files within the workspace +func (p *Provider) Grep(ctx context.Context, args *GrepArgs) (*GrepResult, error) { + workDir, err := p.getWorkDir(ctx) + if err != nil { + return nil, err + } + + // Compile regex + flags := "" + if args.IgnoreCase { + flags = "(?i)" + } + re, err := regexp.Compile(flags + args.Pattern) + if err != nil { + return nil, fmt.Errorf("invalid pattern: %w", err) + } + + // Determine search path + searchPath := workDir + if args.Path != "" { + searchPath, err = validatePath(workDir, args.Path) + if err != nil { + return nil, err + } + } + + // Walk directory + maxResults := args.MaxResults + if maxResults == 0 { + maxResults = 100 + } + + var matches []GrepMatch + err = filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // Skip errors + } + + // Skip directories and non-text files + if info.IsDir() || !isTextFile(path) { + return nil + } + + // Read file + content, err := os.ReadFile(path) + if err != nil { + return nil // Skip unreadable files + } + + // Search lines + lines := bytes.Split(content, []byte("\n")) + for i, line := range lines { + if re.Match(line) { + relPath, _ := filepath.Rel(workDir, path) + matches = append(matches, GrepMatch{ + File: relPath, + Line: i + 1, + Content: string(line), + }) + + if len(matches) >= maxResults { + return filepath.SkipAll + } + } + } + + return nil + }) + + if err != nil && err != filepath.SkipAll { + return nil, fmt.Errorf("grep failed: %w", err) + } + + return &GrepResult{ + Matches: matches, + Total: len(matches), + }, nil +} + +// isTextFile checks if a file is likely a text file based on extension +func isTextFile(path string) bool { + // Simple heuristic: check extension + ext := strings.ToLower(filepath.Ext(path)) + textExts := []string{ + ".txt", ".md", ".go", ".ts", ".js", ".tsx", ".jsx", + ".py", ".rb", ".java", ".c", ".cpp", ".h", ".hpp", + ".json", ".yaml", ".yml", ".toml", ".xml", ".html", + ".css", ".scss", ".sql", ".sh", ".bash", + } + + for _, textExt := range textExts { + if ext == textExt { + return true + } + } + + return false +} diff --git a/experimental/apps-mcp/lib/providers/workspace/pathutil.go b/experimental/apps-mcp/lib/providers/workspace/pathutil.go new file mode 100644 index 0000000000..09933a53c0 --- /dev/null +++ b/experimental/apps-mcp/lib/providers/workspace/pathutil.go @@ -0,0 +1,10 @@ +package workspace + +import ( + "github.com/databricks/cli/experimental/apps-mcp/lib/pathutil" +) + +// validatePath ensures the given user path is safe and within the base directory +func validatePath(baseDir, userPath string) (string, error) { + return pathutil.ValidatePath(baseDir, userPath) +} diff --git a/experimental/apps-mcp/lib/providers/workspace/provider.go b/experimental/apps-mcp/lib/providers/workspace/provider.go new file mode 100644 index 0000000000..06345322dc --- /dev/null +++ b/experimental/apps-mcp/lib/providers/workspace/provider.go @@ -0,0 +1,269 @@ +package workspace + +import ( + "context" + "encoding/json" + "fmt" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + mcpsdk "github.com/databricks/cli/experimental/apps-mcp/lib/mcp" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers" + "github.com/databricks/cli/experimental/apps-mcp/lib/session" + "github.com/databricks/cli/libs/log" +) + +func init() { + providers.Register("workspace", func(ctx context.Context, cfg *mcp.Config, sess *session.Session) (providers.Provider, error) { + return NewProvider(ctx, sess) + }, providers.ProviderConfig{ + EnabledWhen: func(cfg *mcp.Config) bool { + return cfg.WithWorkspaceTools + }, + }) +} + +// Provider implements the workspace provider for file operations +type Provider struct { + session *session.Session + ctx context.Context +} + +// NewProvider creates a new workspace provider +func NewProvider(ctx context.Context, sess *session.Session) (*Provider, error) { + return &Provider{ + session: sess, + ctx: ctx, + }, nil +} + +// Name returns the name of the provider. +func (p *Provider) Name() string { + return "workspace" +} + +// getWorkDir retrieves the working directory from the session via context +func (p *Provider) getWorkDir(ctx context.Context) (string, error) { + workDir, err := session.GetWorkDir(ctx) + if err != nil { + return "", fmt.Errorf( + "workspace directory not set - please run scaffold_data_app first to initialize your project: %w", + err, + ) + } + return workDir, nil +} + +// RegisterTools registers all workspace tools with the MCP server +func (p *Provider) RegisterTools(server *mcpsdk.Server) error { + log.Info(p.ctx, "Registering workspace tools") + + // Register read_file + type ReadFileInput struct { + FilePath string `json:"file_path" jsonschema:"required" jsonschema_description:"Path to file relative to workspace"` + Offset int `json:"offset,omitempty" jsonschema_description:"Line number to start reading (1-indexed)"` + Limit int `json:"limit,omitempty" jsonschema_description:"Number of lines to read"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "read_file", + Description: "Read file contents with line numbers. Default: reads up to 2000 lines from beginning. Lines >2000 chars truncated.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args ReadFileInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "read_file called: file_path=%s", args.FilePath) + + readArgs := &ReadFileArgs{ + FilePath: args.FilePath, + Offset: args.Offset, + Limit: args.Limit, + } + + content, err := p.ReadFile(ctx, readArgs) + if err != nil { + return nil, nil, err + } + + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: content}, + }, + }, nil, nil + }), + ) + + // Register write_file + type WriteFileInput struct { + FilePath string `json:"file_path" jsonschema:"required" jsonschema_description:"Path to file relative to workspace"` + Content string `json:"content" jsonschema:"required" jsonschema_description:"Content to write"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "write_file", + Description: "Write content to a file", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args WriteFileInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "write_file called: file_path=%s", args.FilePath) + + writeArgs := &WriteFileArgs{ + FilePath: args.FilePath, + Content: args.Content, + } + + err := p.WriteFile(ctx, writeArgs) + if err != nil { + return nil, nil, err + } + + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: "File written successfully: " + args.FilePath}, + }, + }, nil, nil + }), + ) + + // Register edit_file + type EditFileInput struct { + FilePath string `json:"file_path" jsonschema:"required" jsonschema_description:"Path to file relative to workspace"` + OldString string `json:"old_string" jsonschema:"required" jsonschema_description:"String to replace (must be unique)"` + NewString string `json:"new_string" jsonschema:"required" jsonschema_description:"Replacement string"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "edit_file", + Description: "Edit file by replacing old_string with new_string. Fails if old_string not unique unless replace_all=true.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args EditFileInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "edit_file called: file_path=%s", args.FilePath) + + editArgs := &EditFileArgs{ + FilePath: args.FilePath, + OldString: args.OldString, + NewString: args.NewString, + } + + err := p.EditFile(ctx, editArgs) + if err != nil { + return nil, nil, err + } + + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: "File edited successfully: " + args.FilePath}, + }, + }, nil, nil + }), + ) + + // Register bash + type BashInput struct { + Command string `json:"command" jsonschema:"required" jsonschema_description:"Bash command to execute"` + Timeout int `json:"timeout,omitempty" jsonschema_description:"Timeout in seconds (default 120)"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "bash", + Description: "Execute bash command in workspace directory. Use for terminal operations (npm, git, etc). Output truncated at 30000 chars.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args BashInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "bash called: command=%s", args.Command) + + bashArgs := &BashArgs{ + Command: args.Command, + Timeout: args.Timeout, + } + + result, err := p.Bash(ctx, bashArgs) + if err != nil { + return nil, nil, err + } + + // Format result as JSON + resultJSON, _ := json.Marshal(result) + + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: string(resultJSON)}, + }, + }, nil, nil + }), + ) + + // Register grep + type GrepInput struct { + Pattern string `json:"pattern" jsonschema:"required" jsonschema_description:"Regular expression pattern to search for"` + Path string `json:"path,omitempty" jsonschema_description:"Limit search to specific path"` + IgnoreCase bool `json:"ignore_case,omitempty" jsonschema_description:"Case insensitive search"` + MaxResults int `json:"max_results,omitempty" jsonschema_description:"Maximum number of results (default 100)"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "grep", + Description: "Search file contents with regex. Returns file:line:content by default. Limit results with head_limit.", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args GrepInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "grep called: pattern=%s", args.Pattern) + + grepArgs := &GrepArgs{ + Pattern: args.Pattern, + Path: args.Path, + IgnoreCase: args.IgnoreCase, + MaxResults: args.MaxResults, + } + + result, err := p.Grep(ctx, grepArgs) + if err != nil { + return nil, nil, err + } + + // Format result as JSON + resultJSON, _ := json.Marshal(result) + + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: string(resultJSON)}, + }, + }, nil, nil + }), + ) + + // Register glob + type GlobInput struct { + Pattern string `json:"pattern" jsonschema:"required" jsonschema_description:"File pattern to match (e.g., '*.go', 'src/**/*.ts')"` + } + + mcpsdk.AddTool(server, + &mcpsdk.Tool{ + Name: "glob", + Description: "Find files matching a glob pattern", + }, + session.WrapToolHandler(p.session, func(ctx context.Context, req *mcpsdk.CallToolRequest, args GlobInput) (*mcpsdk.CallToolResult, any, error) { + log.Debugf(ctx, "glob called: pattern=%s", args.Pattern) + + globArgs := &GlobArgs{ + Pattern: args.Pattern, + } + + result, err := p.Glob(ctx, globArgs) + if err != nil { + return nil, nil, err + } + + // Format result as JSON + resultJSON, _ := json.Marshal(result) + + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{Type: "text", Text: string(resultJSON)}, + }, + }, nil, nil + }), + ) + + log.Infof(p.ctx, "Registered workspace tools: count=%d", 6) + return nil +} diff --git a/experimental/apps-mcp/lib/sandbox/dagger/stub.go b/experimental/apps-mcp/lib/sandbox/dagger/stub.go new file mode 100644 index 0000000000..8bd49e2d1d --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/dagger/stub.go @@ -0,0 +1,69 @@ +// Package dagger provides a stub implementation for Dagger-based sandbox. +// This is a placeholder for future containerized execution support. +package dagger + +import ( + "context" + "errors" + + "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox" +) + +func init() { + sandbox.Register(sandbox.TypeDagger, func(cfg *sandbox.Config) (sandbox.Sandbox, error) { + return nil, errors.New("dagger sandbox is not implemented") + }) +} + +// DaggerSandbox is a stub implementation that always returns errors. +type DaggerSandbox struct{} + +// Exec is not implemented. +func (d *DaggerSandbox) Exec(ctx context.Context, command string) (*sandbox.ExecResult, error) { + return nil, errors.New("dagger sandbox is not implemented") +} + +// WriteFile is not implemented. +func (d *DaggerSandbox) WriteFile(ctx context.Context, path, content string) error { + return errors.New("dagger sandbox is not implemented") +} + +// WriteFiles is not implemented. +func (d *DaggerSandbox) WriteFiles(ctx context.Context, files map[string]string) error { + return errors.New("dagger sandbox is not implemented") +} + +// ReadFile is not implemented. +func (d *DaggerSandbox) ReadFile(ctx context.Context, path string) (string, error) { + return "", errors.New("dagger sandbox is not implemented") +} + +// DeleteFile is not implemented. +func (d *DaggerSandbox) DeleteFile(ctx context.Context, path string) error { + return errors.New("dagger sandbox is not implemented") +} + +// ListDirectory is not implemented. +func (d *DaggerSandbox) ListDirectory(ctx context.Context, path string) ([]string, error) { + return nil, errors.New("dagger sandbox is not implemented") +} + +// SetWorkdir is not implemented. +func (d *DaggerSandbox) SetWorkdir(ctx context.Context, path string) error { + return errors.New("dagger sandbox is not implemented") +} + +// ExportDirectory is not implemented. +func (d *DaggerSandbox) ExportDirectory(ctx context.Context, containerPath, hostPath string) (string, error) { + return "", errors.New("dagger sandbox is not implemented") +} + +// RefreshFromHost is not implemented. +func (d *DaggerSandbox) RefreshFromHost(ctx context.Context, hostPath, containerPath string) error { + return errors.New("dagger sandbox is not implemented") +} + +// Close is not implemented. +func (d *DaggerSandbox) Close() error { + return nil +} diff --git a/experimental/apps-mcp/lib/sandbox/doc.go b/experimental/apps-mcp/lib/sandbox/doc.go new file mode 100644 index 0000000000..0b91548f2f --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/doc.go @@ -0,0 +1,28 @@ +/* +Package sandbox provides an abstraction for executing commands and file operations. + +The sandbox interface allows tools to operate on files and execute commands +in a platform-agnostic way, supporting both local and containerized execution. + +Interface: + + type Sandbox interface { + Exec(ctx, command) (*ExecResult, error) + WriteFile(ctx, path, content) error + ReadFile(ctx, path) (string, error) + // ... other file operations + } + +Implementations: + +- local: Direct filesystem and shell access with security constraints +- dagger: Not implemented (stub only) + +Security: + +The sandbox enforces security constraints: +- Path validation (prevent directory traversal) +- Symlink resolution +- Relative path requirements +*/ +package sandbox diff --git a/experimental/apps-mcp/lib/sandbox/factory.go b/experimental/apps-mcp/lib/sandbox/factory.go new file mode 100644 index 0000000000..cfc6fae828 --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/factory.go @@ -0,0 +1,74 @@ +package sandbox + +import ( + "fmt" + "time" +) + +// Type represents the type of sandbox implementation. +type Type string + +const ( + // TypeLocal uses the local filesystem for sandbox operations. + TypeLocal Type = "local" + + // TypeDagger uses Dagger containers for sandbox operations. + TypeDagger Type = "dagger" +) + +// Config holds the configuration for creating a sandbox. +type Config struct { + BaseDir string + Timeout time.Duration +} + +// Option is a functional option for configuring sandbox creation. +type Option func(*Config) + +// WithBaseDir sets the base directory for the sandbox. +// This is required for local sandboxes. +func WithBaseDir(dir string) Option { + return func(c *Config) { + c.BaseDir = dir + } +} + +// WithTimeout sets the default timeout for sandbox operations. +func WithTimeout(d time.Duration) Option { + return func(c *Config) { + c.Timeout = d + } +} + +// NewConfig creates a config from options. +func NewConfig(opts ...Option) *Config { + cfg := &Config{ + Timeout: 5 * time.Minute, // Default timeout + } + for _, opt := range opts { + opt(cfg) + } + return cfg +} + +// FactoryFunc is a function that creates a sandbox from configuration. +type FactoryFunc func(*Config) (Sandbox, error) + +var factories = make(map[Type]FactoryFunc) + +// Register registers a sandbox factory for a specific type. +func Register(typ Type, factory FactoryFunc) { + factories[typ] = factory +} + +// New creates a new sandbox of the specified type with the given options. +func New(typ Type, opts ...Option) (Sandbox, error) { + cfg := NewConfig(opts...) + + factory, ok := factories[typ] + if !ok { + return nil, fmt.Errorf("unknown sandbox type: %s", typ) + } + + return factory(cfg) +} diff --git a/experimental/apps-mcp/lib/sandbox/local/local.go b/experimental/apps-mcp/lib/sandbox/local/local.go new file mode 100644 index 0000000000..19161dd3fb --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/local/local.go @@ -0,0 +1,421 @@ +// Package local provides a filesystem-based sandbox implementation. +package local + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "sync" + + "github.com/databricks/cli/experimental/apps-mcp/lib/fileutil" + "github.com/databricks/cli/experimental/apps-mcp/lib/sandbox" +) + +func init() { + sandbox.Register(sandbox.TypeLocal, func(cfg *sandbox.Config) (sandbox.Sandbox, error) { + if cfg.BaseDir == "" { + return nil, errors.New("base directory required for local sandbox") + } + return NewLocalSandbox(cfg.BaseDir) + }) +} + +// LocalSandbox implements the Sandbox interface using the local filesystem. +// All operations are restricted to a base directory for security. +type LocalSandbox struct { + baseDir string // Base directory for all operations (absolute path) + workDir string // Current working directory (relative to baseDir) + mu sync.RWMutex // Protects workDir +} + +// NewLocalSandbox creates a new LocalSandbox with the specified base directory. +// The base directory is created if it doesn't exist. +func NewLocalSandbox(baseDir string) (*LocalSandbox, error) { + absBase, err := filepath.Abs(baseDir) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + + if err := os.MkdirAll(absBase, 0o755); err != nil { + return nil, fmt.Errorf("failed to create base directory: %w", err) + } + + resolvedBase, err := filepath.EvalSymlinks(absBase) + if err != nil { + return nil, fmt.Errorf("failed to resolve base directory symlinks: %w", err) + } + + return &LocalSandbox{ + baseDir: resolvedBase, + workDir: ".", + }, nil +} + +// Exec executes a command in the sandbox. +func (s *LocalSandbox) Exec(ctx context.Context, command string) (*sandbox.ExecResult, error) { + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + absWorkDir, err := ValidatePath(s.baseDir, workDir) + if err != nil { + return nil, fmt.Errorf("invalid working directory: %w", err) + } + + cmd := exec.CommandContext(ctx, "sh", "-c", command) + cmd.Dir = absWorkDir + + var stdout, stderr strings.Builder + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Run() + + exitCode := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } else { + return nil, fmt.Errorf("failed to execute command: %w", err) + } + } + + return &sandbox.ExecResult{ + ExitCode: exitCode, + Stdout: stdout.String(), + Stderr: stderr.String(), + }, nil +} + +// WriteFile writes content to a file. +func (s *LocalSandbox) WriteFile(ctx context.Context, path, content string) error { + if filepath.IsAbs(path) { + return fmt.Errorf("absolute paths not allowed: %s", path) + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + fullPath := filepath.Join(workDir, path) + + absPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return err + } + + if err := fileutil.AtomicWriteFile(absPath, []byte(content), 0o644); err != nil { + return err + } + + return nil +} + +// WriteFiles writes multiple files atomically (or as close as possible). +func (s *LocalSandbox) WriteFiles(ctx context.Context, files map[string]string) error { + for path := range files { + if filepath.IsAbs(path) { + return fmt.Errorf("absolute paths not allowed: %s", path) + } + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + validatedPaths := make(map[string]string, len(files)) + for path := range files { + fullPath := filepath.Join(workDir, path) + absPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return fmt.Errorf("invalid path %s: %w", path, err) + } + validatedPaths[path] = absPath + } + + for path, content := range files { + absPath := validatedPaths[path] + + parentDir := filepath.Dir(absPath) + if err := os.MkdirAll(parentDir, 0o755); err != nil { + return fmt.Errorf("failed to create parent directory for %s: %w", path, err) + } + + if err := os.WriteFile(absPath, []byte(content), 0o644); err != nil { + return fmt.Errorf("failed to write file %s: %w", path, err) + } + } + + return nil +} + +// ReadFile reads a file's content. +func (s *LocalSandbox) ReadFile(ctx context.Context, path string) (string, error) { + if filepath.IsAbs(path) { + return "", fmt.Errorf("absolute paths not allowed: %s", path) + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + fullPath := filepath.Join(workDir, path) + + absPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return "", err + } + + content, err := os.ReadFile(absPath) + if err != nil { + return "", fmt.Errorf("failed to read file: %w", err) + } + + return string(content), nil +} + +// DeleteFile deletes a file. +func (s *LocalSandbox) DeleteFile(ctx context.Context, path string) error { + if filepath.IsAbs(path) { + return fmt.Errorf("absolute paths not allowed: %s", path) + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + fullPath := filepath.Join(workDir, path) + + absPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return err + } + + if err := os.Remove(absPath); err != nil { + return fmt.Errorf("failed to delete file: %w", err) + } + + return nil +} + +// ListDirectory lists files and directories. +func (s *LocalSandbox) ListDirectory(ctx context.Context, path string) ([]string, error) { + if filepath.IsAbs(path) { + return nil, fmt.Errorf("absolute paths not allowed: %s", path) + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + fullPath := filepath.Join(workDir, path) + + absPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return nil, err + } + + entries, err := os.ReadDir(absPath) + if err != nil { + return nil, fmt.Errorf("failed to read directory: %w", err) + } + + names := make([]string, 0, len(entries)) + for _, entry := range entries { + names = append(names, entry.Name()) + } + sort.Strings(names) + + return names, nil +} + +// SetWorkdir changes the current working directory. +func (s *LocalSandbox) SetWorkdir(ctx context.Context, path string) error { + if filepath.IsAbs(path) { + return fmt.Errorf("absolute paths not allowed: %s", path) + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + fullPath := filepath.Join(workDir, path) + + absPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return err + } + + info, err := os.Stat(absPath) + if err != nil { + return fmt.Errorf("failed to stat path: %w", err) + } + if !info.IsDir() { + return fmt.Errorf("path is not a directory: %s", path) + } + + relPath, err := filepath.Rel(s.baseDir, absPath) + if err != nil { + return fmt.Errorf("failed to compute relative path: %w", err) + } + + s.mu.Lock() + s.workDir = relPath + s.mu.Unlock() + + return nil +} + +// ExportDirectory exports a directory from the sandbox to the host. +// For LocalSandbox, this is essentially a copy operation. +func (s *LocalSandbox) ExportDirectory(ctx context.Context, containerPath, hostPath string) (string, error) { + if filepath.IsAbs(containerPath) { + return "", fmt.Errorf("absolute paths not allowed: %s", containerPath) + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + fullPath := filepath.Join(workDir, containerPath) + + absSrcPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return "", fmt.Errorf("invalid container path: %w", err) + } + + absHostPath, err := filepath.Abs(hostPath) + if err != nil { + return "", fmt.Errorf("invalid host path: %w", err) + } + + if err := os.MkdirAll(absHostPath, 0o755); err != nil { + return "", fmt.Errorf("failed to create host directory: %w", err) + } + + err = copyDir(absSrcPath, absHostPath) + if err != nil { + return "", fmt.Errorf("failed to copy directory: %w", err) + } + + return absHostPath, nil +} + +// RefreshFromHost imports files from the host into the sandbox. +func (s *LocalSandbox) RefreshFromHost(ctx context.Context, hostPath, containerPath string) error { + if filepath.IsAbs(containerPath) { + return fmt.Errorf("absolute paths not allowed: %s", containerPath) + } + + absHostPath, err := filepath.Abs(hostPath) + if err != nil { + return fmt.Errorf("invalid host path: %w", err) + } + + s.mu.RLock() + workDir := s.workDir + s.mu.RUnlock() + + fullPath := filepath.Join(workDir, containerPath) + + absContainerPath, err := ValidatePath(s.baseDir, fullPath) + if err != nil { + return fmt.Errorf("invalid container path: %w", err) + } + + err = copyDir(absHostPath, absContainerPath) + if err != nil { + return fmt.Errorf("failed to copy from host: %w", err) + } + + return nil +} + +// Close releases resources. For LocalSandbox, this is a no-op. +func (s *LocalSandbox) Close() error { + return nil +} + +// copyDir recursively copies a directory. +func copyDir(src, dst string) error { + // Get source directory info + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + // Create destination directory + if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return err + } + + // Read source directory + entries, err := os.ReadDir(src) + if err != nil { + return err + } + + // Copy each entry + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + // Recursively copy subdirectory + if err := copyDir(srcPath, dstPath); err != nil { + return err + } + } else { + // Copy file + if err := copyFile(srcPath, dstPath); err != nil { + return err + } + } + } + + return nil +} + +// copyFile copies a single file. +func copyFile(src, dst string) (err error) { + // Open source file + srcFile, openErr := os.Open(src) + if openErr != nil { + return openErr + } + defer func() { + if closeErr := srcFile.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + + // Get source file info for permissions + srcInfo, statErr := srcFile.Stat() + if statErr != nil { + return statErr + } + + // Create destination file + dstFile, createErr := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, srcInfo.Mode()) + if createErr != nil { + return createErr + } + defer func() { + if closeErr := dstFile.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + + // Copy contents + if _, copyErr := io.Copy(dstFile, srcFile); copyErr != nil { + return copyErr + } + + return nil +} diff --git a/experimental/apps-mcp/lib/sandbox/local/local_bench_test.go b/experimental/apps-mcp/lib/sandbox/local/local_bench_test.go new file mode 100644 index 0000000000..351407542c --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/local/local_bench_test.go @@ -0,0 +1,197 @@ +package local + +import ( + "context" + "fmt" + "testing" +) + +func BenchmarkLocalSandbox_WriteFile(b *testing.B) { + baseDir := b.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + b.Fatalf("NewLocalSandbox() error = %v", err) + } + defer sb.Close() + + ctx := context.Background() + content := "benchmark content" + + b.ResetTimer() + for i := range b.N { + path := fmt.Sprintf("bench_%d.txt", i) + err := sb.WriteFile(ctx, path, content) + if err != nil { + b.Fatalf("WriteFile() error = %v", err) + } + } +} + +func BenchmarkLocalSandbox_ReadFile(b *testing.B) { + baseDir := b.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + b.Fatalf("NewLocalSandbox() error = %v", err) + } + defer sb.Close() + + ctx := context.Background() + content := "benchmark content" + + // Write a test file + err = sb.WriteFile(ctx, "bench.txt", content) + if err != nil { + b.Fatalf("WriteFile() error = %v", err) + } + + b.ResetTimer() + for range b.N { + _, err := sb.ReadFile(ctx, "bench.txt") + if err != nil { + b.Fatalf("ReadFile() error = %v", err) + } + } +} + +func BenchmarkLocalSandbox_WriteFiles(b *testing.B) { + baseDir := b.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + b.Fatalf("NewLocalSandbox() error = %v", err) + } + defer sb.Close() + + ctx := context.Background() + + // Prepare files map + files := map[string]string{ + "file1.txt": "content 1", + "file2.txt": "content 2", + "file3.txt": "content 3", + "file4.txt": "content 4", + "file5.txt": "content 5", + } + + b.ResetTimer() + for i := range b.N { + // Modify paths to avoid conflicts + benchFiles := make(map[string]string, len(files)) + for path, content := range files { + benchFiles[fmt.Sprintf("bench_%d_%s", i, path)] = content + } + + err := sb.WriteFiles(ctx, benchFiles) + if err != nil { + b.Fatalf("WriteFiles() error = %v", err) + } + } +} + +func BenchmarkLocalSandbox_ListDirectory(b *testing.B) { + baseDir := b.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + b.Fatalf("NewLocalSandbox() error = %v", err) + } + defer sb.Close() + + ctx := context.Background() + + // Create some files + for i := range 100 { + path := fmt.Sprintf("file_%d.txt", i) + err := sb.WriteFile(ctx, path, "content") + if err != nil { + b.Fatalf("WriteFile() error = %v", err) + } + } + + b.ResetTimer() + for range b.N { + _, err := sb.ListDirectory(ctx, ".") + if err != nil { + b.Fatalf("ListDirectory() error = %v", err) + } + } +} + +func BenchmarkLocalSandbox_Exec(b *testing.B) { + baseDir := b.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + b.Fatalf("NewLocalSandbox() error = %v", err) + } + defer sb.Close() + + ctx := context.Background() + + b.ResetTimer() + for range b.N { + _, err := sb.Exec(ctx, "echo benchmark") + if err != nil { + b.Fatalf("Exec() error = %v", err) + } + } +} + +func BenchmarkLocalSandbox_ExecComplex(b *testing.B) { + baseDir := b.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + b.Fatalf("NewLocalSandbox() error = %v", err) + } + defer sb.Close() + + ctx := context.Background() + + // Create a test file + err = sb.WriteFile(ctx, "input.txt", "line 1\nline 2\nline 3\n") + if err != nil { + b.Fatalf("WriteFile() error = %v", err) + } + + b.ResetTimer() + for range b.N { + _, err := sb.Exec(ctx, "grep 'line' input.txt | wc -l") + if err != nil { + b.Fatalf("Exec() error = %v", err) + } + } +} + +func BenchmarkValidatePath(b *testing.B) { + baseDir := b.TempDir() + + b.ResetTimer() + for range b.N { + _, err := ValidatePath(baseDir, "test/path/file.txt") + if err != nil { + b.Fatalf("ValidatePath() error = %v", err) + } + } +} + +func BenchmarkValidatePath_Existing(b *testing.B) { + baseDir := b.TempDir() + + // Create an existing file + sb, err := NewLocalSandbox(baseDir) + if err != nil { + b.Fatalf("NewLocalSandbox() error = %v", err) + } + defer sb.Close() + + ctx := context.Background() + err = sb.WriteFile(ctx, "existing.txt", "content") + if err != nil { + b.Fatalf("WriteFile() error = %v", err) + } + + b.ResetTimer() + for range b.N { + _, err := ValidatePath(baseDir, "existing.txt") + if err != nil { + b.Fatalf("ValidatePath() error = %v", err) + } + } +} diff --git a/experimental/apps-mcp/lib/sandbox/local/local_test.go b/experimental/apps-mcp/lib/sandbox/local/local_test.go new file mode 100644 index 0000000000..2607fd754e --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/local/local_test.go @@ -0,0 +1,517 @@ +package local + +import ( + "context" + "os" + "path/filepath" + "strings" + "sync" + "testing" +) + +func TestNewLocalSandbox(t *testing.T) { + baseDir := t.TempDir() + + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + if sb == nil { + t.Fatal("NewLocalSandbox() returned nil sandbox") + } + + // Verify base directory was created + if _, err := os.Stat(baseDir); os.IsNotExist(err) { + t.Error("NewLocalSandbox() did not create base directory") + } +} + +func TestLocalSandbox_WriteFile(t *testing.T) { + tests := []struct { + name string + path string + content string + wantErr bool + errMsg string + }{ + { + name: "simple file", + path: "test.txt", + content: "hello world", + wantErr: false, + }, + { + name: "nested path", + path: "subdir/test.txt", + content: "nested content", + wantErr: false, + }, + { + name: "deeply nested path", + path: "a/b/c/d/test.txt", + content: "deep content", + wantErr: false, + }, + { + name: "path traversal attempt", + path: "../outside.txt", + content: "malicious", + wantErr: true, + errMsg: "outside base directory", + }, + { + name: "absolute path attempt", + path: "/etc/passwd", + content: "malicious", + wantErr: true, + errMsg: "absolute paths not allowed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + err = sb.WriteFile(ctx, tt.path, tt.content) + + if tt.wantErr { + if err == nil { + t.Errorf("WriteFile() expected error, got nil") + } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("WriteFile() error = %v, want error containing %q", err, tt.errMsg) + } + } else { + if err != nil { + t.Errorf("WriteFile() unexpected error: %v", err) + } + // Verify file was written + content, err := sb.ReadFile(ctx, tt.path) + if err != nil { + t.Errorf("ReadFile() error after WriteFile: %v", err) + } + if content != tt.content { + t.Errorf("ReadFile() = %q, want %q", content, tt.content) + } + } + }) + } +} + +func TestLocalSandbox_ReadFile(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Write a test file + testContent := "test content" + err = sb.WriteFile(ctx, "test.txt", testContent) + if err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + // Read it back + content, err := sb.ReadFile(ctx, "test.txt") + if err != nil { + t.Errorf("ReadFile() error = %v", err) + } + if content != testContent { + t.Errorf("ReadFile() = %q, want %q", content, testContent) + } + + // Try to read non-existent file + _, err = sb.ReadFile(ctx, "nonexistent.txt") + if err == nil { + t.Error("ReadFile() expected error for non-existent file, got nil") + } +} + +func TestLocalSandbox_WriteFiles(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + files := map[string]string{ + "file1.txt": "content 1", + "dir/file2.txt": "content 2", + "dir/file3.txt": "content 3", + "other/file4.txt": "content 4", + } + + err = sb.WriteFiles(ctx, files) + if err != nil { + t.Errorf("WriteFiles() error = %v", err) + } + + // Verify all files were written + for path, expectedContent := range files { + content, err := sb.ReadFile(ctx, path) + if err != nil { + t.Errorf("ReadFile(%q) error = %v", path, err) + } + if content != expectedContent { + t.Errorf("ReadFile(%q) = %q, want %q", path, content, expectedContent) + } + } +} + +func TestLocalSandbox_DeleteFile(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Write a file + err = sb.WriteFile(ctx, "test.txt", "content") + if err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + // Delete it + err = sb.DeleteFile(ctx, "test.txt") + if err != nil { + t.Errorf("DeleteFile() error = %v", err) + } + + // Verify it's gone + _, err = sb.ReadFile(ctx, "test.txt") + if err == nil { + t.Error("ReadFile() expected error after deletion, got nil") + } +} + +func TestLocalSandbox_ListDirectory(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Create some files + files := map[string]string{ + "file1.txt": "content1", + "file2.txt": "content2", + "dir/file3.txt": "content3", + } + err = sb.WriteFiles(ctx, files) + if err != nil { + t.Fatalf("WriteFiles() error = %v", err) + } + + // List root directory + entries, err := sb.ListDirectory(ctx, ".") + if err != nil { + t.Errorf("ListDirectory() error = %v", err) + } + + // Should contain file1.txt, file2.txt, and dir + expected := []string{"dir", "file1.txt", "file2.txt"} + if len(entries) != len(expected) { + t.Errorf("ListDirectory() returned %d entries, want %d", len(entries), len(expected)) + } + + for i, want := range expected { + if i >= len(entries) || entries[i] != want { + t.Errorf("ListDirectory()[%d] = %q, want %q", i, entries[i], want) + } + } +} + +func TestLocalSandbox_SetWorkdir(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Create a subdirectory + subdir := "subdir" + err = os.MkdirAll(filepath.Join(baseDir, subdir), 0o755) + if err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + + // Set working directory + err = sb.SetWorkdir(ctx, subdir) + if err != nil { + t.Errorf("SetWorkdir() error = %v", err) + } + + // Verify workdir was set (by checking internal state) + if sb.workDir != subdir { + t.Errorf("SetWorkdir() workDir = %q, want %q", sb.workDir, subdir) + } + + // Try to set to non-existent directory + err = sb.SetWorkdir(ctx, "nonexistent") + if err == nil { + t.Error("SetWorkdir() expected error for non-existent directory, got nil") + } +} + +func TestLocalSandbox_Exec(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + tests := []struct { + name string + command string + wantExit int + wantStdout string + wantStderr string + wantErr bool + }{ + { + name: "simple echo", + command: "echo hello", + wantExit: 0, + wantStdout: "hello", + wantErr: false, + }, + { + name: "command with exit code", + command: "exit 42", + wantExit: 42, + wantErr: false, + }, + { + name: "stderr output", + command: "echo error >&2", + wantExit: 0, + wantStderr: "error", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := sb.Exec(ctx, tt.command) + + if tt.wantErr { + if err == nil { + t.Error("Exec() expected error, got nil") + } + return + } + + if err != nil { + t.Errorf("Exec() unexpected error: %v", err) + return + } + + if result.ExitCode != tt.wantExit { + t.Errorf("Exec() ExitCode = %d, want %d", result.ExitCode, tt.wantExit) + } + + if tt.wantStdout != "" && !strings.Contains(result.Stdout, tt.wantStdout) { + t.Errorf("Exec() Stdout = %q, want to contain %q", result.Stdout, tt.wantStdout) + } + + if tt.wantStderr != "" && !strings.Contains(result.Stderr, tt.wantStderr) { + t.Errorf("Exec() Stderr = %q, want to contain %q", result.Stderr, tt.wantStderr) + } + }) + } +} + +func TestLocalSandbox_PathTraversal(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Try various path traversal attacks + attacks := []string{ + "../../../etc/passwd", + "subdir/../../etc/passwd", + "./../outside.txt", + } + + for _, attack := range attacks { + t.Run("attack:"+attack, func(t *testing.T) { + err := sb.WriteFile(ctx, attack, "malicious") + if err == nil { + t.Errorf("WriteFile(%q) expected error, got nil", attack) + return + } + if !strings.Contains(err.Error(), "outside base directory") { + t.Errorf("WriteFile(%q) error = %v, want 'outside base directory'", attack, err) + } + }) + } +} + +func TestLocalSandbox_SymlinkEscape(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Create a symlink pointing outside the base directory + linkPath := filepath.Join(baseDir, "escape") + err = os.Symlink("/tmp", linkPath) + if err != nil { + t.Fatalf("Symlink() error = %v", err) + } + + // Try to write through the symlink + err = sb.WriteFile(ctx, "escape/malicious.txt", "content") + if err == nil { + t.Error("WriteFile() through escape symlink expected error, got nil") + } +} + +func TestLocalSandbox_Concurrent(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Run multiple operations concurrently + const numGoroutines = 10 + const numOpsPerGoroutine = 100 + + var wg sync.WaitGroup + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(id int) { + defer wg.Done() + + for j := range numOpsPerGoroutine { + path := filepath.Join("concurrent", string(rune('a'+id)), "test.txt") + content := string(rune('0' + j%10)) + + // Write + if err := sb.WriteFile(ctx, path, content); err != nil { + t.Errorf("Concurrent WriteFile() error: %v", err) + return + } + + // Read + if _, err := sb.ReadFile(ctx, path); err != nil { + t.Errorf("Concurrent ReadFile() error: %v", err) + return + } + } + }(i) + } + + wg.Wait() +} + +func TestLocalSandbox_ExportDirectory(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Create some files + files := map[string]string{ + "export/file1.txt": "content1", + "export/file2.txt": "content2", + } + err = sb.WriteFiles(ctx, files) + if err != nil { + t.Fatalf("WriteFiles() error = %v", err) + } + + // Export directory + hostPath := t.TempDir() + exportedPath, err := sb.ExportDirectory(ctx, "export", hostPath) + if err != nil { + t.Errorf("ExportDirectory() error = %v", err) + } + + // Verify files were exported + for relPath := range files { + baseName := filepath.Base(relPath) + exportedFile := filepath.Join(exportedPath, baseName) + if _, err := os.Stat(exportedFile); os.IsNotExist(err) { + t.Errorf("ExportDirectory() did not export %s", baseName) + } + } +} + +func TestLocalSandbox_RefreshFromHost(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + ctx := context.Background() + + // Create a host directory with files + hostDir := t.TempDir() + hostFile := filepath.Join(hostDir, "host-file.txt") + err = os.WriteFile(hostFile, []byte("host content"), 0o644) + if err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + // Refresh from host + err = sb.RefreshFromHost(ctx, hostDir, "imported") + if err != nil { + t.Errorf("RefreshFromHost() error = %v", err) + } + + // Verify file was imported + content, err := sb.ReadFile(ctx, "imported/host-file.txt") + if err != nil { + t.Errorf("ReadFile() after RefreshFromHost error = %v", err) + } + if content != "host content" { + t.Errorf("ReadFile() = %q, want %q", content, "host content") + } +} + +func TestLocalSandbox_Close(t *testing.T) { + baseDir := t.TempDir() + sb, err := NewLocalSandbox(baseDir) + if err != nil { + t.Fatalf("NewLocalSandbox() error = %v", err) + } + + // Close should not error + err = sb.Close() + if err != nil { + t.Errorf("Close() error = %v", err) + } +} diff --git a/experimental/apps-mcp/lib/sandbox/local/pathutil.go b/experimental/apps-mcp/lib/sandbox/local/pathutil.go new file mode 100644 index 0000000000..52d566acc1 --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/local/pathutil.go @@ -0,0 +1,30 @@ +package local + +import ( + "github.com/databricks/cli/experimental/apps-mcp/lib/pathutil" +) + +// ValidatePath ensures that the user-provided path is within baseDir and +// returns the absolute path. This prevents directory traversal attacks. +// +// Deprecated: Use pathutil.ValidatePath instead. +func ValidatePath(baseDir, userPath string) (string, error) { + return pathutil.ValidatePath(baseDir, userPath) +} + +// MustValidatePath is like ValidatePath but panics on error. +// Use this only in tests or when you know the path is safe. +// +// Deprecated: Use pathutil.MustValidatePath instead. +func MustValidatePath(baseDir, userPath string) string { + return pathutil.MustValidatePath(baseDir, userPath) +} + +// RelativePath returns the relative path from baseDir to targetPath. +// Both paths should be absolute. Returns an error if targetPath is not +// within baseDir. +// +// Deprecated: Use pathutil.RelativePath instead. +func RelativePath(baseDir, targetPath string) (string, error) { + return pathutil.RelativePath(baseDir, targetPath) +} diff --git a/experimental/apps-mcp/lib/sandbox/local/pathutil_test.go b/experimental/apps-mcp/lib/sandbox/local/pathutil_test.go new file mode 100644 index 0000000000..b1fde64391 --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/local/pathutil_test.go @@ -0,0 +1,190 @@ +package local + +import ( + "os" + "path/filepath" + "testing" +) + +func TestValidatePath(t *testing.T) { + // Create a temp directory for testing + baseDir := t.TempDir() + + tests := []struct { + name string + userPath string + wantErr bool + errMsg string + setup func() error + }{ + { + name: "simple relative path", + userPath: "test.txt", + wantErr: false, + }, + { + name: "nested relative path", + userPath: "subdir/test.txt", + wantErr: false, + }, + { + name: "path with dots", + userPath: "subdir/./test.txt", + wantErr: false, + }, + { + name: "path traversal attempt", + userPath: "../outside.txt", + wantErr: true, + errMsg: "outside base directory", + }, + { + name: "absolute path traversal", + userPath: "/../etc/passwd", + wantErr: true, + errMsg: "absolute paths not allowed", + }, + { + name: "symlink escape attempt", + userPath: "symlink/test.txt", + wantErr: true, + errMsg: "outside base directory", + setup: func() error { + // Create a symlink pointing outside baseDir + linkPath := filepath.Join(baseDir, "symlink") + return os.Symlink("/tmp", linkPath) + }, + }, + { + name: "valid symlink within base", + userPath: "goodlink/test.txt", + wantErr: false, + setup: func() error { + // Create a target directory + targetDir := filepath.Join(baseDir, "target") + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return err + } + // Create a symlink to it + linkPath := filepath.Join(baseDir, "goodlink") + return os.Symlink(targetDir, linkPath) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Run setup if provided + if tt.setup != nil { + if err := tt.setup(); err != nil { + t.Fatalf("setup failed: %v", err) + } + } + + result, err := ValidatePath(baseDir, tt.userPath) + + if tt.wantErr { + if err == nil { + t.Errorf("ValidatePath() expected error containing %q, got nil", tt.errMsg) + } else if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) { + t.Errorf("ValidatePath() error = %v, want error containing %q", err, tt.errMsg) + } + } else { + if err != nil { + t.Errorf("ValidatePath() unexpected error: %v", err) + } + // Verify result is absolute + if !filepath.IsAbs(result) { + t.Errorf("ValidatePath() result %q is not absolute", result) + } + // Verify result starts with baseDir + absBase, _ := filepath.Abs(baseDir) + if !contains(result, absBase) { + t.Errorf("ValidatePath() result %q does not start with base %q", result, absBase) + } + } + }) + } +} + +func TestRelativePath(t *testing.T) { + tests := []struct { + name string + baseDir string + targetPath string + want string + wantErr bool + }{ + { + name: "simple relative path", + baseDir: "/tmp/base", + targetPath: "/tmp/base/file.txt", + want: "file.txt", + wantErr: false, + }, + { + name: "nested relative path", + baseDir: "/tmp/base", + targetPath: "/tmp/base/sub/dir/file.txt", + want: "sub/dir/file.txt", + wantErr: false, + }, + { + name: "outside base directory", + baseDir: "/tmp/base", + targetPath: "/tmp/other/file.txt", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := RelativePath(tt.baseDir, tt.targetPath) + if tt.wantErr { + if err == nil { + t.Errorf("RelativePath() expected error, got nil") + } + } else { + if err != nil { + t.Errorf("RelativePath() unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("RelativePath() = %q, want %q", got, tt.want) + } + } + }) + } +} + +func TestMustValidatePath(t *testing.T) { + baseDir := t.TempDir() + + // Should not panic for valid path + result := MustValidatePath(baseDir, "test.txt") + if result == "" { + t.Error("MustValidatePath() returned empty string") + } + + // Should panic for invalid path + defer func() { + if r := recover(); r == nil { + t.Error("MustValidatePath() should panic for invalid path") + } + }() + MustValidatePath(baseDir, "../outside.txt") +} + +// Helper function to check if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > 0 && len(substr) > 0 && someContains(s, substr))) +} + +func someContains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/experimental/apps-mcp/lib/sandbox/sandbox.go b/experimental/apps-mcp/lib/sandbox/sandbox.go new file mode 100644 index 0000000000..63997edf38 --- /dev/null +++ b/experimental/apps-mcp/lib/sandbox/sandbox.go @@ -0,0 +1,58 @@ +// Package sandbox provides an abstraction for executing commands and managing +// files in an isolated environment. +package sandbox + +import "context" + +// ExecResult contains the result of executing a command in the sandbox. +type ExecResult struct { + ExitCode int + Stdout string + Stderr string +} + +// Sandbox defines the interface for executing commands and managing files +// in an isolated environment. Implementations may use local filesystem +// or other isolation mechanisms. +type Sandbox interface { + // Exec executes a command in the sandbox and returns the result. + // The command is executed in the current working directory. + Exec(ctx context.Context, command string) (*ExecResult, error) + + // WriteFile writes content to a file at the specified path. + // Parent directories are created if they don't exist. + // Paths must be relative to the sandbox root. + WriteFile(ctx context.Context, path, content string) error + + // WriteFiles writes multiple files atomically. + // If any write fails, all writes should be rolled back. + WriteFiles(ctx context.Context, files map[string]string) error + + // ReadFile reads the content of a file at the specified path. + // Returns an error if the file doesn't exist or cannot be read. + ReadFile(ctx context.Context, path string) (string, error) + + // DeleteFile deletes the file at the specified path. + // Returns an error if the file doesn't exist or cannot be deleted. + DeleteFile(ctx context.Context, path string) error + + // ListDirectory lists all files and directories in the specified path. + // Returns a sorted list of names (not full paths). + ListDirectory(ctx context.Context, path string) ([]string, error) + + // SetWorkdir changes the current working directory for future commands. + // The path must be relative to the sandbox root. + SetWorkdir(ctx context.Context, path string) error + + // ExportDirectory exports a directory from the sandbox to the host filesystem. + // Returns the absolute path to the exported directory on the host. + ExportDirectory(ctx context.Context, containerPath, hostPath string) (string, error) + + // RefreshFromHost imports files from the host filesystem into the sandbox. + // This is useful for incremental updates without recreating the sandbox. + RefreshFromHost(ctx context.Context, hostPath, containerPath string) error + + // Close releases any resources held by the sandbox. + // After calling Close, the sandbox should not be used. + Close() error +} diff --git a/experimental/apps-mcp/lib/server/doc.go b/experimental/apps-mcp/lib/server/doc.go new file mode 100644 index 0000000000..71f469c715 --- /dev/null +++ b/experimental/apps-mcp/lib/server/doc.go @@ -0,0 +1,39 @@ +/* +Package server implements the Model Context Protocol (MCP) server. + +The MCP server provides AI agents with tools to interact with Databricks. +It uses the official MCP Go SDK and supports stdio transport. + +Usage: + + ctx := context.Background() + cfg := &config.Config{ + WarehouseID: "abc123", + } + server := server.NewServer(cfg, ctx) + err := server.Run(ctx) + +Architecture: + +The server uses a provider-based architecture where each provider +registers its tools independently. Providers include: + +- Databricks: Query catalogs, schemas, tables, execute SQL +- IO: Scaffold and validate TypeScript applications +- Workspace: File operations in project directories +- Deployment: Deploy applications (optional) + +Session Management: + +Sessions track state across tool calls including: +- Working directory (set by scaffold, used by workspace tools) +- Metrics and telemetry +- Trajectory logging (JSONL history) + +Sandbox: + +Tools execute in a sandbox abstraction that can be: +- Local: Direct filesystem and shell access +- Dagger: Not implemented (stub only) +*/ +package server diff --git a/experimental/apps-mcp/lib/server/health.go b/experimental/apps-mcp/lib/server/health.go new file mode 100644 index 0000000000..3af828c4d2 --- /dev/null +++ b/experimental/apps-mcp/lib/server/health.go @@ -0,0 +1,62 @@ +package server + +import ( + "context" + "fmt" + "time" +) + +// HealthStatus represents the health status of the server +type HealthStatus struct { + Healthy bool `json:"healthy"` + Providers map[string]string `json:"providers"` + Timestamp time.Time `json:"timestamp"` +} + +// CheckHealth checks the health of all registered providers +func (s *Server) CheckHealth(ctx context.Context) *HealthStatus { + status := &HealthStatus{ + Healthy: true, + Providers: make(map[string]string), + Timestamp: time.Now(), + } + + // Check databricks provider + if err := s.checkDatabricksHealth(ctx); err != nil { + status.Providers["databricks"] = fmt.Sprintf("unhealthy: %v", err) + status.Healthy = false + } else { + status.Providers["databricks"] = "healthy" + } + + // I/O provider doesn't need health checks (no external dependencies) + status.Providers["io"] = "healthy" + + // Check workspace provider if enabled + if s.config.WithWorkspaceTools { + status.Providers["workspace"] = "healthy" + } + + // Check deployment provider if enabled + if s.config.AllowDeployment { + status.Providers["deployment"] = "healthy" + } + + return status +} + +// checkDatabricksHealth performs a basic health check for Databricks +func (s *Server) checkDatabricksHealth(ctx context.Context) error { + // Create a short-lived context for the health check + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + _ = timeoutCtx + + // For now, just check if provider was registered successfully + // A more thorough check would call the Databricks API (e.g., list catalogs) + // but that requires storing a reference to the provider + + // If Databricks is in required providers, registration succeeded + // So we consider it healthy + return nil +} diff --git a/experimental/apps-mcp/lib/server/server.go b/experimental/apps-mcp/lib/server/server.go new file mode 100644 index 0000000000..d6f2417a4a --- /dev/null +++ b/experimental/apps-mcp/lib/server/server.go @@ -0,0 +1,205 @@ +// Package server provides the main MCP server implementation with provider registration and lifecycle management. +package server + +import ( + "context" + + mcp "github.com/databricks/cli/experimental/apps-mcp/lib" + mcpsdk "github.com/databricks/cli/experimental/apps-mcp/lib/mcp" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers/databricks" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers/deployment" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers/io" + "github.com/databricks/cli/experimental/apps-mcp/lib/providers/workspace" + "github.com/databricks/cli/experimental/apps-mcp/lib/session" + "github.com/databricks/cli/experimental/apps-mcp/lib/trajectory" + "github.com/databricks/cli/internal/build" + "github.com/databricks/cli/libs/log" +) + +// Server manages the MCP server lifecycle, provider registration, and session tracking. +type Server struct { + server *mcpsdk.Server + config *mcp.Config + session *session.Session + tracker *trajectory.Tracker +} + +// NewServer creates and initializes a new MCP server instance. +// It creates a session, trajectory tracker, and prepares the server for provider registration. +func NewServer(ctx context.Context, cfg *mcp.Config) *Server { + impl := &mcpsdk.Implementation{ + Name: "databricks-apps-mcp", + Version: build.GetInfo().Version, + } + + server := mcpsdk.NewServer(impl, nil) + sess := session.NewSession() + + tracker, err := trajectory.NewTracker(ctx, sess, cfg) + if err != nil { + log.Warnf(ctx, "failed to create trajectory tracker: %v", err) + tracker = nil + } + + sess.Tracker = tracker + + return &Server{ + server: server, + config: cfg, + session: sess, + tracker: tracker, + } +} + +// RegisterTools registers all configured providers and their tools with the server. +// Databricks and IO providers are always registered, while workspace and deployment +// providers are conditional based on configuration flags. +func (s *Server) RegisterTools(ctx context.Context) error { + log.Info(ctx, "Registering tools") + + // Always register databricks provider + if err := s.registerDatabricksProvider(ctx); err != nil { + return err + } + + // Always register io provider + if err := s.registerIOProvider(ctx); err != nil { + return err + } + + // Register workspace provider if enabled + if s.config.WithWorkspaceTools { + log.Info(ctx, "Workspace provider enabled") + if err := s.registerWorkspaceProvider(ctx); err != nil { + return err + } + } else { + log.Info(ctx, "Workspace provider disabled (enable with --with-workspace-tools)") + } + + // Register deployment provider if enabled + if s.config.AllowDeployment { + log.Info(ctx, "Deployment provider enabled") + if err := s.registerDeploymentProvider(ctx); err != nil { + return err + } + } else { + log.Info(ctx, "Deployment provider disabled (enable with allow_deployment: true)") + } + + return nil +} + +// registerDatabricksProvider registers the Databricks provider +func (s *Server) registerDatabricksProvider(ctx context.Context) error { + log.Info(ctx, "Registering Databricks provider") + + // Add session to context + ctx = session.WithSession(ctx, s.session) + + provider, err := databricks.NewProvider(ctx, s.config, s.session) + if err != nil { + return err + } + + if err := provider.RegisterTools(s.server); err != nil { + return err + } + + return nil +} + +// registerIOProvider registers the I/O provider +func (s *Server) registerIOProvider(ctx context.Context) error { + log.Info(ctx, "Registering I/O provider") + + // Add session to context + ctx = session.WithSession(ctx, s.session) + + provider, err := io.NewProvider(ctx, s.config.IoConfig, s.session) + if err != nil { + return err + } + + if err := provider.RegisterTools(s.server); err != nil { + return err + } + + return nil +} + +// registerWorkspaceProvider registers the workspace provider +func (s *Server) registerWorkspaceProvider(ctx context.Context) error { + log.Info(ctx, "Registering workspace provider") + + // Add session to context + ctx = session.WithSession(ctx, s.session) + + provider, err := workspace.NewProvider(ctx, s.session) + if err != nil { + return err + } + + if err := provider.RegisterTools(s.server); err != nil { + return err + } + + return nil +} + +// registerDeploymentProvider registers the deployment provider +func (s *Server) registerDeploymentProvider(ctx context.Context) error { + log.Info(ctx, "Registering deployment provider") + + // Add session to context + ctx = session.WithSession(ctx, s.session) + + provider, err := deployment.NewProvider(ctx, s.config, s.session) + if err != nil { + return err + } + + if err := provider.RegisterTools(s.server); err != nil { + return err + } + + return nil +} + +// Run starts the MCP server with STDIO transport and blocks until the context is cancelled. +// The server communicates via standard input/output following the MCP protocol. +func (s *Server) Run(ctx context.Context) error { + log.Info(ctx, "Starting MCP server with STDIO transport") + + transport := mcpsdk.NewStdioTransport() + if err := s.server.Run(ctx, transport); err != nil { + log.Errorf(ctx, "Server failed: error=%v", err) + return err + } + + return nil +} + +// Shutdown gracefully shuts down the server, closing the trajectory tracker and releasing resources. +func (s *Server) Shutdown(ctx context.Context) error { + log.Info(ctx, "Shutting down MCP server") + + if s.tracker != nil { + if err := s.tracker.Close(); err != nil { + log.Warnf(ctx, "failed to close trajectory tracker: error=%v", err) + } + } + + return nil +} + +// GetServer returns the underlying MCP SDK server instance for testing purposes. +func (s *Server) GetServer() *mcpsdk.Server { + return s.server +} + +// GetTracker returns the trajectory tracker used for recording tool calls. +// Providers use this to wrap their tool handlers for automatic trajectory logging. +func (s *Server) GetTracker() *trajectory.Tracker { + return s.tracker +} diff --git a/experimental/apps-mcp/lib/session/engine_guide.go b/experimental/apps-mcp/lib/session/engine_guide.go new file mode 100644 index 0000000000..061e4a8944 --- /dev/null +++ b/experimental/apps-mcp/lib/session/engine_guide.go @@ -0,0 +1,47 @@ +package session + +import ( + "context" + _ "embed" + + "github.com/databricks/cli/experimental/apps-mcp/lib/mcp" +) + +//go:embed initialization_message.md +var initializationMessageText string + +// InitializationMessage is the initialization message injected on first tool call. +var InitializationMessage = initializationMessageText + +// TrajectoryTracker interface to avoid import cycle +type TrajectoryTracker interface { + RecordToolCall(toolName string, args any, result *mcp.CallToolResult, err error) +} + +// WrapToolHandler wraps a tool handler to inject the ENGINE_GUIDE on first tool call +// and record trajectory if enabled +func WrapToolHandler[T any](session *Session, handler func(ctx context.Context, req *mcp.CallToolRequest, args T) (*mcp.CallToolResult, any, error)) func(ctx context.Context, req *mcp.CallToolRequest, args T) (*mcp.CallToolResult, any, error) { + return func(ctx context.Context, req *mcp.CallToolRequest, args T) (*mcp.CallToolResult, any, error) { + // Check if this is the first tool call + isFirst := session.IsFirstTool() + + // Execute the original handler + result, data, err := handler(ctx, req, args) + + // Record trajectory if tracker is available + if session.Tracker != nil { + if tracker, ok := session.Tracker.(TrajectoryTracker); ok { + tracker.RecordToolCall(req.Params.Name, args, result, err) + } + } + + // If this was the first call and execution was successful, prepend the guide + if err == nil && isFirst && result != nil && len(result.Content) > 0 { + if textContent, ok := result.Content[0].(*mcp.TextContent); ok { + textContent.Text = InitializationMessage + "\n\n---\n\n" + textContent.Text + } + } + + return result, data, err + } +} diff --git a/experimental/apps-mcp/lib/session/initialization_message.md b/experimental/apps-mcp/lib/session/initialization_message.md new file mode 100644 index 0000000000..1c711d719c --- /dev/null +++ b/experimental/apps-mcp/lib/session/initialization_message.md @@ -0,0 +1,18 @@ +Your session in Edda MCP has been successfully initialized. Here are the guidelines to follow while working on projects using edda_mcp tools: + +## Project State Management: +This project uses a state file (`.edda_state`) managed by edda MCP to enforce the correct workflow order: +1. **Scaffolded**: `scaffold_data_app` creates project structure from template (starts in this state) +2. **Validated**: `validate_data_app` runs build + tests in sandbox, computes BLAKE3 checksum of package.json and all core source files +3. **Deployed**: `deploy_databricks_app` deploys to Databricks Apps, but ONLY if checksum hasn't changed since validation + +Re-validation is allowed (Deployed → Validated) to update the checksum after intentional changes. The edda MCP tools enforce these state transitions and prevent invalid state changes. + +## Workflow: +- Projects MUST end with validate_project to verify build + tests pass +- Bias towards backend code when the task allows implementation in multiple places +- Always add tests for what you're implementing, put them next to the code (e.g. src/*.test.ts) +- When working with Databricks or other services, use real API calls in tests (no mocks) to verify end-to-end functionality, unless explicitly instructed otherwise. It can be done on subset of data if applicable. +- Do NOT create summary files, reports, or README unless explicitly requested +- When not sure about the user's intent, ask clarifying questions before proceeding. For example, if user asks for "a data app to analyze sales data", ask for more details on data sources and analysis goals. Do not make assumptions regarding their needs and data sources. +- However, stick to the technical stack initialized by the `scaffold_data_app` as it has been approved by the management and battle-tested in production. diff --git a/experimental/apps-mcp/lib/session/session.go b/experimental/apps-mcp/lib/session/session.go new file mode 100644 index 0000000000..49a565d431 --- /dev/null +++ b/experimental/apps-mcp/lib/session/session.go @@ -0,0 +1,142 @@ +package session + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "math/big" + "sync" + "time" +) + +// contextKey is the type for context keys +type contextKey int + +const ( + workDirKey contextKey = iota + sessionKey +) + +// Session represents an MCP session with state tracking +type Session struct { + ID string + workDir string + mu sync.RWMutex + startTime time.Time + firstTool bool + toolCalls int + Tracker any // trajectory tracker (to avoid import cycle) +} + +// NewSession creates a new session +func NewSession() *Session { + return &Session{ + ID: generateID(), + startTime: time.Now(), + firstTool: true, + } +} + +// WithSession adds session to context +func WithSession(ctx context.Context, s *Session) context.Context { + return context.WithValue(ctx, sessionKey, s) +} + +// GetSession retrieves session from context +func GetSession(ctx context.Context) (*Session, error) { + if v := ctx.Value(sessionKey); v != nil { + return v.(*Session), nil + } + return nil, errors.New("session not found in context") +} + +// SetWorkDir sets the working directory via context +func SetWorkDir(ctx context.Context, dir string) error { + sess, err := GetSession(ctx) + if err != nil { + return err + } + + sess.mu.Lock() + defer sess.mu.Unlock() + + if sess.workDir != "" { + return errors.New("work directory already set") + } + + sess.workDir = dir + return nil +} + +// GetWorkDir retrieves work directory via context +func GetWorkDir(ctx context.Context) (string, error) { + sess, err := GetSession(ctx) + if err != nil { + return "", err + } + + sess.mu.RLock() + defer sess.mu.RUnlock() + + if sess.workDir == "" { + return "", errors.New("work directory not set") + } + + return sess.workDir, nil +} + +// IsFirstTool returns true if this is the first tool call in the session +// and sets the flag to false for subsequent calls +func (s *Session) IsFirstTool() bool { + s.mu.Lock() + defer s.mu.Unlock() + + if s.firstTool { + s.firstTool = false + return true + } + return false +} + +// IncrementToolCalls increments the tool call counter +func (s *Session) IncrementToolCalls() int { + s.mu.Lock() + defer s.mu.Unlock() + + s.toolCalls++ + return s.toolCalls +} + +// GetToolCalls returns the number of tool calls made in this session +func (s *Session) GetToolCalls() int { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.toolCalls +} + +// GetUptime returns the duration since the session started +func (s *Session) GetUptime() time.Duration { + return time.Since(s.startTime) +} + +// generateID generates a unique session ID +func generateID() string { + return fmt.Sprintf("%d-%s", time.Now().Unix(), randomString(8)) +} + +// randomString generates a random string of the given length using crypto/rand +func randomString(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, length) + charsetLen := big.NewInt(int64(len(charset))) + for i := range b { + n, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + panic(fmt.Sprintf("crypto/rand failed: %v", err)) + } + b[i] = charset[n.Int64()] + } + return string(b) +} diff --git a/experimental/apps-mcp/lib/session/session_test.go b/experimental/apps-mcp/lib/session/session_test.go new file mode 100644 index 0000000000..be950a3b52 --- /dev/null +++ b/experimental/apps-mcp/lib/session/session_test.go @@ -0,0 +1,150 @@ +package session + +import ( + "context" + "testing" + "time" +) + +func TestNewSession(t *testing.T) { + s := NewSession() + + if s.ID == "" { + t.Error("Session ID should not be empty") + } + + if s.workDir != "" { + t.Error("workDir should be empty initially") + } + + if !s.firstTool { + t.Error("firstTool should be true initially") + } + + if s.toolCalls != 0 { + t.Error("toolCalls should be 0 initially") + } +} + +func TestSession_SetWorkDir(t *testing.T) { + s := NewSession() + ctx := WithSession(context.Background(), s) + + // First set should succeed + err := SetWorkDir(ctx, "/tmp/test") + if err != nil { + t.Fatalf("First SetWorkDir failed: %v", err) + } + + // Second set should fail + err = SetWorkDir(ctx, "/tmp/test2") + if err == nil { + t.Error("Second SetWorkDir should fail") + } + + // Verify the work dir is set correctly + workDir, err := GetWorkDir(ctx) + if err != nil { + t.Fatalf("GetWorkDir failed: %v", err) + } + + if workDir != "/tmp/test" { + t.Errorf("Expected work dir '/tmp/test', got '%s'", workDir) + } +} + +func TestSession_GetWorkDir_NotSet(t *testing.T) { + s := NewSession() + ctx := WithSession(context.Background(), s) + + _, err := GetWorkDir(ctx) + if err == nil { + t.Error("GetWorkDir should fail when work dir is not set") + } +} + +func TestSession_IsFirstTool(t *testing.T) { + s := NewSession() + + // First call should return true + if !s.IsFirstTool() { + t.Error("First IsFirstTool call should return true") + } + + // Subsequent calls should return false + if s.IsFirstTool() { + t.Error("Second IsFirstTool call should return false") + } + + if s.IsFirstTool() { + t.Error("Third IsFirstTool call should return false") + } +} + +func TestSession_ToolCalls(t *testing.T) { + s := NewSession() + + if s.GetToolCalls() != 0 { + t.Error("Initial tool calls should be 0") + } + + count := s.IncrementToolCalls() + if count != 1 { + t.Errorf("Expected count 1, got %d", count) + } + + if s.GetToolCalls() != 1 { + t.Errorf("Expected tool calls 1, got %d", s.GetToolCalls()) + } + + count = s.IncrementToolCalls() + if count != 2 { + t.Errorf("Expected count 2, got %d", count) + } + + if s.GetToolCalls() != 2 { + t.Errorf("Expected tool calls 2, got %d", s.GetToolCalls()) + } +} + +func TestSession_GetUptime(t *testing.T) { + s := NewSession() + + // Sleep a bit to ensure uptime is measurable + time.Sleep(10 * time.Millisecond) + + uptime := s.GetUptime() + if uptime < 10*time.Millisecond { + t.Errorf("Expected uptime >= 10ms, got %v", uptime) + } +} + +func TestGenerateID(t *testing.T) { + id1 := generateID() + id2 := generateID() + + if id1 == "" { + t.Error("Generated ID should not be empty") + } + + if id1 == id2 { + t.Error("Generated IDs should be unique") + } +} + +func TestRandomString(t *testing.T) { + s1 := randomString(8) + s2 := randomString(8) + + if len(s1) != 8 { + t.Errorf("Expected length 8, got %d", len(s1)) + } + + if len(s2) != 8 { + t.Errorf("Expected length 8, got %d", len(s2)) + } + + if s1 == s2 { + t.Error("Random strings should be different") + } +} diff --git a/experimental/apps-mcp/lib/templates/embed.go b/experimental/apps-mcp/lib/templates/embed.go new file mode 100644 index 0000000000..6ffb38ece9 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/embed.go @@ -0,0 +1,18 @@ +package templates + +import ( + "embed" +) + +//go:embed trpc/* +var trpcFS embed.FS + +// GetTRPCTemplate returns the embedded TRPC template +func GetTRPCTemplate() Template { + return NewEmbeddedTemplate( + "TRPC", + "Modern full-stack template with tRPC, TypeScript, and React", + trpcFS, + "trpc", + ) +} diff --git a/experimental/apps-mcp/lib/templates/template.go b/experimental/apps-mcp/lib/templates/template.go new file mode 100644 index 0000000000..7eba20f7fc --- /dev/null +++ b/experimental/apps-mcp/lib/templates/template.go @@ -0,0 +1,75 @@ +package templates + +import ( + "embed" + "fmt" + "io/fs" + "strings" +) + +// Template represents a project template +type Template interface { + Name() string + Description() string + Files() (map[string]string, error) +} + +// EmbeddedTemplate is a template loaded from an embedded filesystem +type EmbeddedTemplate struct { + name string + description string + fsys embed.FS + root string +} + +// NewEmbeddedTemplate creates a new embedded template +func NewEmbeddedTemplate(name, desc string, fsys embed.FS, root string) *EmbeddedTemplate { + return &EmbeddedTemplate{ + name: name, + description: desc, + fsys: fsys, + root: root, + } +} + +// Name returns the template name +func (t *EmbeddedTemplate) Name() string { + return t.name +} + +// Description returns the template description +func (t *EmbeddedTemplate) Description() string { + return t.description +} + +// Files returns a map of file paths to their contents +func (t *EmbeddedTemplate) Files() (map[string]string, error) { + files := make(map[string]string) + + err := fs.WalkDir(t.fsys, t.root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + // Read file content + content, err := fs.ReadFile(t.fsys, path) + if err != nil { + return fmt.Errorf("failed to read %s: %w", path, err) + } + + // Remove root prefix from path + relativePath := strings.TrimPrefix(path, t.root+"/") + if relativePath == "" { + relativePath = path + } + files[relativePath] = string(content) + + return nil + }) + + return files, err +} diff --git a/experimental/apps-mcp/lib/templates/trpc/.dabgent_state b/experimental/apps-mcp/lib/templates/trpc/.dabgent_state new file mode 100644 index 0000000000..d332f0242d --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/.dabgent_state @@ -0,0 +1,3 @@ +{ + "state": "Scaffolded" +} diff --git a/experimental/apps-mcp/lib/templates/trpc/.dockerignore b/experimental/apps-mcp/lib/templates/trpc/.dockerignore new file mode 100644 index 0000000000..c561b24ad2 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/.dockerignore @@ -0,0 +1,14 @@ +node_modules +dist +.git +*.log +.env* +.DS_Store +npm-debug.log* +yarn-debug.log* +yarn-error.log* +coverage +.vscode +.idea +*.swp +*.swo diff --git a/experimental/apps-mcp/lib/templates/trpc/.gitignore b/experimental/apps-mcp/lib/templates/trpc/.gitignore new file mode 100644 index 0000000000..13a31a0d12 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ +/.pnp +.pnp.js + +# Testing +/coverage + +# Build outputs +/build +/dist +/.next/ +/out/ +server/public + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories +.idea/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db diff --git a/experimental/apps-mcp/lib/templates/trpc/CLAUDE.md b/experimental/apps-mcp/lib/templates/trpc/CLAUDE.md new file mode 100644 index 0000000000..6559eb0351 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/CLAUDE.md @@ -0,0 +1,116 @@ +TypeScript full-stack template with tRPC for type-safe API communication between React frontend and Node.js backend. Use this when building type-safe TypeScript applications with the following structure: +- server/: Node.js backend with tRPC API +- client/: React frontend with tRPC client + +## Testing Guidelines: + +**CRITICAL**: Use Node.js native test runner only. Do NOT import vitest, jest, or supertest. +Put tests next to the code (e.g. src/*.test.ts) + +```typescript +import { test } from "node:test"; +import { strict as assert } from "node:assert"; +``` + +## Databricks Type Handling: + +- **QueryResult access**: `executeQuery()` returns `{rows: T[], rowCount: number}`. Always use `.rows` property: `const {rows} = await client.executeQuery(...)` or `result.rows.map(...)` +- **Type imports**: Use `import type { T }` (not `import { T }`) when `verbatimModuleSyntax` is enabled +- **Column access**: Use bracket notation `row['column_name']` (TypeScript strict mode requirement) +- **DATE/TIMESTAMP columns**: Databricks returns Date objects. Use `z.coerce.date()` in schemas (never `z.string()` for dates) +- **Dynamic properties**: Cast explicitly `row['order_id'] as number` + +## Frontend Styling Guidelines: + +### Component Structure Pattern: +- Use container with proper spacing: `
` +- Page titles: `

Title

` +- Forms: Use `space-y-4` for vertical spacing between inputs +- Cards: Use shadcn Card components or `border p-4 rounded-md` for item display +- Grids: Use `grid gap-4` for list layouts + +### Example App Structure: +```tsx +
+

Page Title

+
{/* form inputs */}
+
{/* list items */}
+
+``` + +### Tailwind Usage: +- Use Tailwind classes directly in JSX +- Avoid @apply unless creating reusable component styles +- When using @apply, only in @layer components (never @layer base) +- Template has CSS variables defined - use via Tailwind (bg-background, text-foreground, etc.) + +### Typography & Spacing: +- Headings: text-2xl font-bold with mb-4 +- Secondary text: text-foreground/70 +- Card titles: text-xl font-semibold +- Form spacing: space-y-4 between inputs, mb-8 after forms +- Grid/list spacing: gap-4 for consistent item spacing + +### Component Organization: +Create separate components when: +- Logic exceeds ~100 lines +- Component is reused in multiple places +- Component has distinct responsibility (e.g., ProductForm, ProductList) +File structure: +- Shared UI: client/src/components/ui/ +- Feature components: client/src/components/FeatureName.tsx + +### Visual Design: +- Adjust visual mood to match user prompt, prefer clean and modern visually appealing aesthetics, but avoid overly flashy designs - keep it professional and user-friendly; +- Use shadcn/radix components (Button, Input, Card, etc.) for consistent UI +- Forms should have loading states: `disabled={isLoading}` +- Show empty states with helpful text when no data exists + +### Best Practices: +- Always fetch real data from tRPC (never use mock/hardcoded data) +- Handle nullable fields: `value={field || ''}` for inputs +- Type all callbacks explicitly: `onChange={(e: React.ChangeEvent) => ...}` +- Use proper relative imports for server types: `import type { Product } from '../../server/src/schema'` + +## Data Visualization with Recharts + +The template includes Recharts for data visualization. Use Databricks brand colors for chart elements: `['#40d1f5', '#4462c9', '#EB1600', '#0B2026', '#4A4A4A', '#353a4a']` (apply via `stroke` or `fill` props). + +### Basic Chart Pattern: +```tsx +import { useState, useEffect } from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import { Card, CardContent, CardHeader, CardTitle } from './components/ui/card'; +import { trpc } from './utils/trpc'; + +function MyDashboard() { + const [data, setData] = useState<{ name: string; value: number }[]>([]); + const [error, setError] = useState(null); + + useEffect(() => { + // fetch from Databricks via tRPC + trpc.getMetrics.query() + .then(setData) + .catch((err) => setError(err.message)); + }, []); + + return ( + + + My Metrics + + + + + + + + + + + + + + ); +} +``` diff --git a/experimental/apps-mcp/lib/templates/trpc/Dockerfile b/experimental/apps-mcp/lib/templates/trpc/Dockerfile new file mode 100644 index 0000000000..653eae3ad3 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/Dockerfile @@ -0,0 +1,63 @@ +# Multi-stage Dockerfile for tRPC template +FROM node:20-alpine3.22 AS builder + +# install build tools for native Node.js modules (e.g., better-sqlite3, bcrypt) +# python3: required by node-gyp, make/g++: build toolchain +RUN apk add --no-cache python3 make g++ + +WORKDIR /app + +# Copy all package files (may not have lock files if agent modified deps) +COPY client/package*.json ./client/ +COPY server/package*.json ./server/ + +# Install all dependencies with BuildKit cache mount +# Note: Using npm install (not npm ci) in build stage because: +# - Agent may add new dependencies to package.json during development +# - package-lock.json may be missing or out of sync +# - npm install will generate/update the lock file +# The generated lock file is then copied to production stage for reproducible builds +RUN --mount=type=cache,target=/root/.npm \ + cd client && npm install + +RUN --mount=type=cache,target=/root/.npm \ + cd server && npm install + +# Copy server source (needed for client build due to tRPC types) +COPY server/ ./server/ + +# Copy client source and build +COPY client/ ./client/ +RUN cd client && npm run build + +# Stage 2: Production image +FROM node:20-alpine3.22 + +# Install curl for healthcheck and build tools for native modules +RUN apk add --no-cache curl python3 make g++ + +WORKDIR /app + +# Copy server source first (excluding node_modules via .dockerignore) +COPY server/ ./server/ + +# Copy package files including the lock file generated in build stage +# This ensures reproducible production builds with exact dependency versions +COPY --from=builder /app/server/package*.json ./server/ + +# Install dependencies - this will compile native modules for Alpine +RUN --mount=type=cache,target=/root/.npm \ + cd server && npm ci --omit=dev + +# Copy built client from builder stage +RUN mkdir -p server/public +COPY --from=builder /app/client/dist/ ./server/public/ + +# Set working directory to server +WORKDIR /app/server + +# Expose port +EXPOSE 8000 + +# Start the application +CMD ["npm", "start"] diff --git a/experimental/apps-mcp/lib/templates/trpc/client/.gitignore b/experimental/apps-mcp/lib/templates/trpc/client/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/experimental/apps-mcp/lib/templates/trpc/client/components.json b/experimental/apps-mcp/lib/templates/trpc/client/components.json new file mode 100644 index 0000000000..13e1db0b7a --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/experimental/apps-mcp/lib/templates/trpc/client/eslint.config.js b/experimental/apps-mcp/lib/templates/trpc/client/eslint.config.js new file mode 100644 index 0000000000..6b9e5adf03 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/eslint.config.js @@ -0,0 +1,204 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +const noEmptySelectValue = { + meta: { + type: 'problem', + docs: { + description: 'Disallow empty string values in Select item components', + }, + messages: { + emptySelectValue: 'Select item components cannot have an empty string as value. Use a meaningful default value.', + }, + }, + create(context) { + return { + JSXAttribute(node) { + if ( + node.name?.name === 'value' && + node.value?.type === 'Literal' && + node.value.value === '' + ) { + // Check if parent is likely a Select item component + const parentName = node.parent?.name?.name; + if (parentName && ( + // Core select patterns + parentName.includes('Select') || + parentName.includes('Option') || + // These are the most likely to be used with DB data + parentName === 'MenuItem' || + parentName === 'DropdownMenuItem' || + parentName === 'RadioGroupItem' + )) { + context.report({ + node, + messageId: 'emptySelectValue', + }); + } + } + }, + }; + }, +}; + +const noEmptyDynamicSelectValue = { + meta: { + type: 'problem', + docs: { + description: 'Warn about potentially empty dynamic values in Select components', + }, + messages: { + potentiallyEmptyValue: 'This value might be empty when data is not loaded. Consider using a fallback', + }, + }, + create(context) { + return { + JSXAttribute(node) { + if ( + node.name?.name === 'value' && + node.value?.type === 'JSXExpressionContainer' + ) { + const parentName = node.parent?.name?.name; + if (parentName && ( + parentName.includes('Select') || + parentName.includes('Option') || + parentName === 'MenuItem' || + parentName === 'DropdownMenuItem' || + parentName === 'RadioGroupItem' + )) { + const expr = node.value.expression; + + // Skip if it's already a logical expression with fallback + if (expr.type === 'LogicalExpression' && + (expr.operator === '||' || expr.operator === '??')) { + return; + } + + // Skip if it's a conditional expression (ternary) + if (expr.type === 'ConditionalExpression') { + return; + } + + // Only check member expressions (like user.id, item.value) + if (expr.type !== 'MemberExpression') { + return; + } + + // Skip if we're inside a map/filter/forEach callback (likely iterating over existing data) + let parent = node; + while (parent) { + if (parent.type === 'CallExpression' && + parent.callee?.property?.name && + ['map', 'filter', 'forEach', 'reduce'].includes(parent.callee.property.name)) { + return; + } + parent = parent.parent; + } + + // Check if there's a parent conditional that ensures the value exists + let ancestor = node.parent; + while (ancestor) { + // If we're inside an if statement or conditional that checks this value + if (ancestor.type === 'IfStatement' || ancestor.type === 'ConditionalExpression') { + return; // Assume it's been validated + } + ancestor = ancestor.parent; + } + + context.report({ + node, + messageId: 'potentiallyEmptyValue', + }); + } + } + }, + }; + }, +}; + +const noMockData = { + meta: { + type: 'problem', + docs: { + description: 'Disallow mock data and mock implementations', + }, + messages: { + mockVariableName: 'Variable names containing "mock", "dummy", or "fake" are not allowed. Use real data.', + mockComment: 'Comments mentioning mock/dummy/fake data are not allowed. Implement real functionality.', + }, + }, + create(context) { + return { + // Check variable and function names + Identifier(node) { + const name = node.name.toLowerCase(); + if ((name.includes('mock') || name.includes('dummy') || name.includes('fake')) && + (node.parent.type === 'VariableDeclarator' || + node.parent.type === 'FunctionDeclaration' || + node.parent.type === 'FunctionExpression')) { + context.report({ + node, + messageId: 'mockVariableName', + }); + } + }, + // Check comments + Program() { + const sourceCode = context.getSourceCode(); + const comments = sourceCode.getAllComments(); + + comments.forEach(comment => { + const text = comment.value.toLowerCase(); + if (text.includes('mock') || text.includes('dummy') || text.includes('fake')) { + context.report({ + loc: comment.loc, + messageId: 'mockComment', + }); + } + }); + }, + }; + }, +}; + +export default tseslint.config( + { ignores: ['dist', '.cursor/**/*'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'custom': { + rules: { + 'no-empty-select-value': noEmptySelectValue, + 'no-empty-dynamic-select-value': noEmptyDynamicSelectValue, + 'no-mock-data': noMockData, + }, + }, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'custom/no-empty-select-value': 'error', + 'custom/no-empty-dynamic-select-value': 'warn', + 'custom/no-mock-data': 'error', + }, + }, + { + files: ['**/components/ui/**/*.{ts,tsx}'], + rules: { + 'react-refresh/only-export-components': 'off', + }, + }, +) diff --git a/experimental/apps-mcp/lib/templates/trpc/client/index.html b/experimental/apps-mcp/lib/templates/trpc/client/index.html new file mode 100644 index 0000000000..4e28aaf21a --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/index.html @@ -0,0 +1,12 @@ + + + + + created by app.build + + + +
+ + + diff --git a/experimental/apps-mcp/lib/templates/trpc/client/package-lock.json b/experimental/apps-mcp/lib/templates/trpc/client/package-lock.json new file mode 100644 index 0000000000..2fd8ecfd28 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/package-lock.json @@ -0,0 +1,6185 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "4.1.3", + "@radix-ui/react-accordion": "1.2.3", + "@radix-ui/react-alert-dialog": "1.1.6", + "@radix-ui/react-aspect-ratio": "1.1.2", + "@radix-ui/react-avatar": "1.1.3", + "@radix-ui/react-checkbox": "1.1.4", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-context-menu": "2.2.6", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-dropdown-menu": "2.1.6", + "@radix-ui/react-hover-card": "1.1.6", + "@radix-ui/react-label": "2.1.2", + "@radix-ui/react-menubar": "1.1.6", + "@radix-ui/react-navigation-menu": "1.2.5", + "@radix-ui/react-popover": "1.1.6", + "@radix-ui/react-progress": "1.1.2", + "@radix-ui/react-radio-group": "1.2.3", + "@radix-ui/react-scroll-area": "1.2.3", + "@radix-ui/react-select": "2.1.6", + "@radix-ui/react-separator": "1.1.2", + "@radix-ui/react-slider": "1.2.3", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "1.1.3", + "@radix-ui/react-tabs": "1.1.3", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-toggle-group": "1.1.2", + "@radix-ui/react-tooltip": "1.1.8", + "@tailwindcss/vite": "4.0.14", + "@trpc/client": "^11.6.0", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.0", + "date-fns": "^4.1.0", + "embla-carousel-react": "8.5.2", + "lucide-react": "0.479.0", + "next-themes": "0.4.6", + "react": "19.2.0", + "react-day-picker": "^9.11.1", + "react-dom": "19.2.0", + "react-hook-form": "7.54.2", + "react-resizable-panels": "2.1.7", + "recharts": "2.15.0", + "sonner": "2.0.1", + "superjson": "2.2.2", + "tailwind-merge": "3.0.2", + "tailwindcss": "4.0.14", + "tailwindcss-animate": "1.0.7", + "zod": "3.24.2" + }, + "devDependencies": { + "@eslint/js": "9.21.0", + "@types/node": "22.13.10", + "@types/react": "19.2.0", + "@types/react-dom": "19.2.0", + "@vitejs/plugin-react": "4.3.4", + "eslint": "^9.37.0", + "eslint-plugin-react-hooks": "5.1.0", + "eslint-plugin-react-refresh": "0.4.19", + "globals": "15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "8.24.1", + "vite": "^6.3.6", + "vite-tsconfig-paths": "5.1.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.3.tgz", + "integrity": "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.2.tgz", + "integrity": "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz", + "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", + "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.3.tgz", + "integrity": "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz", + "integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz", + "integrity": "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.6.tgz", + "integrity": "sha512-FHq7+3DlXwh/7FOM4i0G4bC4vPjiq89VEEvNF4VMLchGnaUuUbE5uKXMUCjdKaOghEEMeiKa5XCa2Pk4kteWmg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.5.tgz", + "integrity": "sha512-myMHHQUZ3ZLTi8W381/Vu43Ia0NqakkQZ2vzynMmTUtQQ9kNkjzhOwkZC9TAM5R07OZUVIQyHC06f/9JZJpvvA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", + "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", + "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", + "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", + "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz", + "integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", + "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", + "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.2.tgz", + "integrity": "sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz", + "integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==", + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "tailwindcss": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz", + "integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-x64": "4.0.14", + "@tailwindcss/oxide-freebsd-x64": "4.0.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-x64-musl": "4.0.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz", + "integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz", + "integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz", + "integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz", + "integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz", + "integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz", + "integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz", + "integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz", + "integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz", + "integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz", + "integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz", + "integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.14.tgz", + "integrity": "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.0.14", + "@tailwindcss/oxide": "4.0.14", + "lightningcss": "1.29.2", + "tailwindcss": "4.0.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@trpc/client": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.6.0.tgz", + "integrity": "sha512-DyWbYk2hd50BaVrXWVkaUnaSwgAF5g/lfBkXtkF1Aqlk6BtSzGUo3owPkgqQO2I5LwWy1+ra9TsSfBBvIZpTwg==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "peerDependencies": { + "@trpc/server": "11.6.0", + "typescript": ">=5.7.2" + } + }, + "node_modules/@trpc/server": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.6.0.tgz", + "integrity": "sha512-skTso0AWbOZck40jwNeYv++AMZXNWLUWdyk+pB5iVaYmEKTuEeMoPrEudR12VafbEU6tZa8HK3QhBfTYYHDCdg==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "peer": true, + "peerDependencies": { + "typescript": ">=5.7.2" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", + "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", + "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/type-utils": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", + "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", + "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", + "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", + "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.24.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.0.tgz", + "integrity": "sha512-kJTwfCTJYECDkoFWIszsYufE6o22iHUm0RwBhDSrQpkDOVtn0QThsCsTZIsVlYr6wJTi44FhyG2WbSwyBwq/yA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.235", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", + "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.2.tgz", + "integrity": "sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.2.tgz", + "integrity": "sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.5.2", + "embla-carousel-reactive-utils": "8.5.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.2.tgz", + "integrity": "sha512-QC8/hYSK/pEmqEdU1IO5O+XNc/Ptmmq7uCB44vKplgLKhB/l0+yvYx0+Cv0sF6Ena8Srld5vUErZkT+yTahtDg==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.5.2" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.479.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.479.0.tgz", + "integrity": "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz", + "integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", + "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", + "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.1.tgz", + "integrity": "sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", + "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz", + "integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==", + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.1.tgz", + "integrity": "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.24.1", + "@typescript-eslint/parser": "8.24.1", + "@typescript-eslint/utils": "8.24.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/experimental/apps-mcp/lib/templates/trpc/client/package.json b/experimental/apps-mcp/lib/templates/trpc/client/package.json new file mode 100644 index 0000000000..8586a7b178 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/package.json @@ -0,0 +1,77 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "tsc -b && vite build -l info", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "4.1.3", + "@radix-ui/react-accordion": "1.2.3", + "@radix-ui/react-alert-dialog": "1.1.6", + "@radix-ui/react-aspect-ratio": "1.1.2", + "@radix-ui/react-avatar": "1.1.3", + "@radix-ui/react-checkbox": "1.1.4", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-context-menu": "2.2.6", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-dropdown-menu": "2.1.6", + "@radix-ui/react-hover-card": "1.1.6", + "@radix-ui/react-label": "2.1.2", + "@radix-ui/react-menubar": "1.1.6", + "@radix-ui/react-navigation-menu": "1.2.5", + "@radix-ui/react-popover": "1.1.6", + "@radix-ui/react-progress": "1.1.2", + "@radix-ui/react-radio-group": "1.2.3", + "@radix-ui/react-scroll-area": "1.2.3", + "@radix-ui/react-select": "2.1.6", + "@radix-ui/react-separator": "1.1.2", + "@radix-ui/react-slider": "1.2.3", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "1.1.3", + "@radix-ui/react-tabs": "1.1.3", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-toggle-group": "1.1.2", + "@radix-ui/react-tooltip": "1.1.8", + "@tailwindcss/vite": "4.0.14", + "@trpc/client": "^11.6.0", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.0", + "date-fns": "^4.1.0", + "embla-carousel-react": "8.5.2", + "lucide-react": "0.479.0", + "next-themes": "0.4.6", + "react": "19.2.0", + "react-day-picker": "^9.11.1", + "react-dom": "19.2.0", + "react-hook-form": "7.54.2", + "react-resizable-panels": "2.1.7", + "recharts": "2.15.0", + "sonner": "2.0.1", + "superjson": "2.2.2", + "tailwind-merge": "3.0.2", + "tailwindcss": "4.0.14", + "tailwindcss-animate": "1.0.7", + "zod": "3.24.2" + }, + "devDependencies": { + "@eslint/js": "9.21.0", + "@types/node": "22.13.10", + "@types/react": "19.2.0", + "@types/react-dom": "19.2.0", + "@vitejs/plugin-react": "4.3.4", + "eslint": "^9.37.0", + "eslint-plugin-react-hooks": "5.1.0", + "eslint-plugin-react-refresh": "0.4.19", + "globals": "15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "8.24.1", + "vite": "^6.3.6", + "vite-tsconfig-paths": "5.1.4" + } +} diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/App.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/App.tsx new file mode 100644 index 0000000000..7bee7f07da --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/App.tsx @@ -0,0 +1,38 @@ +import { useState, useEffect } from "react"; +import { trpc } from "./utils/trpc"; + +function App() { + const [health, setHealth] = useState<{ + status: string; + timestamp: string; + } | null>(null); + const [error, setError] = useState(null); + + useEffect(() => { + trpc.healthcheck.query() + .then(setHealth) + .catch((err) => setError(err.message)); + }, []); + + return ( +
+

tRPC Template

+

Your tRPC app is running!

+ {health && ( +
+

✓ Server Status: {health.status}

+

+ Timestamp: {health.timestamp} +

+
+ )} + {error && ( +
+

✗ Error: {error}

+
+ )} +
+ ); +} + +export default App; diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/accordion.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/accordion.tsx new file mode 100644 index 0000000000..d21b65f7a5 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/alert-dialog.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000..935eecf3f1 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/alert-dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/alert.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/alert.tsx new file mode 100644 index 0000000000..14213546e5 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/aspect-ratio.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000000..9b491fb88b --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,9 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/avatar.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/avatar.tsx new file mode 100644 index 0000000000..71e428b4ca --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/badge.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/badge.tsx new file mode 100644 index 0000000000..2151ab6b1a --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/breadcrumb.tsx b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000000..eb88f32122 --- /dev/null +++ b/experimental/apps-mcp/lib/templates/trpc/client/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return