diff --git a/V2-ROADMAP.md b/V2-ROADMAP.md index a04c7396..7e1394a8 100644 --- a/V2-ROADMAP.md +++ b/V2-ROADMAP.md @@ -100,24 +100,34 @@ vibe.quit(); --- -## Video Recording +## Video Recording ✅ -**What:** Built-in screen recording of browser sessions. +**Status:** Implemented -**Why deferred:** Adds FFmpeg dependency complexity. Screenshots may be sufficient for debugging. +Record browser sessions as MP4 or WebM video files. Requires FFmpeg to be installed. -**Implementation:** -- Capture screenshots at interval (e.g., 10fps) -- Encode to MP4/WebM via FFmpeg -- Start/stop via `vibium.recording.start` / `vibium.recording.stop` BiDi commands -- JS API: `vibe.startRecording()`, `vibe.stopRecording()` +**CLI:** +```bash +clicker record https://example.com -o recording.mp4 --duration 10 --fps 10 +``` -**When to build:** When users need video artifacts for: -- Test failure debugging -- Demo generation -- Compliance/audit trails +**JavaScript API:** +```typescript +await vibe.startRecording({ fps: 10, format: 'mp4' }); +// ... perform actions ... +const videoPath = await vibe.stopRecording(); +``` -**Estimated effort:** 1 week +**Python API:** +```python +await vibe.start_recording(fps=10, format='mp4') +# ... perform actions ... +video_path = await vibe.stop_recording() +``` + +**BiDi Extension Commands:** +- `vibium:startRecording` - Start recording with options (fps, format, outputPath) +- `vibium:stopRecording` - Stop recording and return video file path --- @@ -222,7 +232,7 @@ const el = await vibe.find("the blue submit button"); Based on likely user demand: 1. ~~**Python client**~~ ✅ shipped -2. **Video recording** — Debugging value, moderate effort +2. ~~**Video recording**~~ ✅ shipped 3. **Network tracing** — DevTools parity 4. **Cortex** — If agents need persistent memory 5. **Retina** — If recording human sessions matters diff --git a/clicker/cmd/clicker/main.go b/clicker/cmd/clicker/main.go index 423b67ff..01514b84 100644 --- a/clicker/cmd/clicker/main.go +++ b/clicker/cmd/clicker/main.go @@ -17,6 +17,7 @@ import ( "github.com/vibium/clicker/internal/paths" "github.com/vibium/clicker/internal/process" "github.com/vibium/clicker/internal/proxy" + "github.com/vibium/clicker/internal/recording" ) var version = "0.1.0" @@ -768,6 +769,122 @@ The server provides browser automation tools: mcpCmd.Flags().String("screenshot-dir", "", "Directory for saving screenshots (default: ~/Pictures/Vibium, use \"\" to disable)") rootCmd.AddCommand(mcpCmd) + recordCmd := &cobra.Command{ + Use: "record [url]", + Short: "Navigate to a URL and record the browser session as a video", + Long: `Record a browser session as a video file. + +This command navigates to a URL, waits for a specified duration while +capturing screenshots, then encodes them into a video using FFmpeg. + +Requires FFmpeg to be installed and available in PATH.`, + Example: ` clicker record https://example.com -o recording.mp4 + # Records the page for 5 seconds (default) + + clicker record https://example.com -o recording.mp4 --duration 10 + # Records for 10 seconds + + clicker record https://example.com -o recording.webm --format webm --fps 15 + # Records as WebM at 15 FPS`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + process.WithCleanup(func() { + url := args[0] + output, _ := cmd.Flags().GetString("output") + duration, _ := cmd.Flags().GetInt("duration") + fps, _ := cmd.Flags().GetInt("fps") + format, _ := cmd.Flags().GetString("format") + + // Check FFmpeg availability + if !recording.IsFFmpegAvailable() { + fmt.Fprintf(os.Stderr, "Error: FFmpeg is not installed or not in PATH\n") + fmt.Fprintf(os.Stderr, "Please install FFmpeg: https://ffmpeg.org/download.html\n") + os.Exit(1) + } + + fmt.Println("Launching browser...") + launchResult, err := browser.Launch(browser.LaunchOptions{Headless: headless}) + if err != nil { + fmt.Fprintf(os.Stderr, "Error launching browser: %v\n", err) + os.Exit(1) + } + defer waitAndClose(launchResult) + + fmt.Println("Connecting to BiDi...") + conn, err := bidi.Connect(launchResult.WebSocketURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err) + os.Exit(1) + } + defer conn.Close() + + client := bidi.NewClient(conn) + + fmt.Printf("Navigating to %s...\n", url) + _, err = client.Navigate("", url) + if err != nil { + fmt.Fprintf(os.Stderr, "Error navigating: %v\n", err) + os.Exit(1) + } + + doWaitOpen() + + // Create screenshot function + screenshotFn := func() (string, error) { + // Simple fix: Focus + scroll to top before screenshot + _, err := client.Evaluate("", ` + window.focus(); + window.scrollTo(0, 0); + document.body.style.overflow = 'visible'; + `) + if err != nil { + return "", err + } + + data, err := client.CaptureScreenshot("") + if err != nil { + return "", err + } + + fmt.Printf("[screenshot] Captured %d bytes\n", len(data)) + return data, nil + } + + + + // Create recorder + recorder := recording.New(screenshotFn, recording.Options{ + FPS: fps, + Format: format, + OutputPath: output, + }) + + fmt.Printf("Starting recording (duration: %ds, fps: %d, format: %s)...\n", duration, fps, format) + if err := recorder.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Error starting recording: %v\n", err) + os.Exit(1) + } + + // Record for specified duration + time.Sleep(time.Duration(duration) * time.Second) + + fmt.Println("Stopping recording and encoding video...") + outputPath, err := recorder.Stop() + if err != nil { + fmt.Fprintf(os.Stderr, "Error stopping recording: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Recording saved to: %s\n", outputPath) + }) + }, + } + recordCmd.Flags().StringP("output", "o", "recording.mp4", "Output file path") + recordCmd.Flags().IntP("duration", "d", 5, "Recording duration in seconds") + recordCmd.Flags().Int("fps", 10, "Frames per second") + recordCmd.Flags().String("format", "mp4", "Output format: mp4 or webm") + rootCmd.AddCommand(recordCmd) + rootCmd.Version = version rootCmd.SetVersionTemplate("Clicker v{{.Version}}\n") diff --git a/clicker/internal/proxy/router.go b/clicker/internal/proxy/router.go index 8adf344e..e9466b9d 100644 --- a/clicker/internal/proxy/router.go +++ b/clicker/internal/proxy/router.go @@ -8,6 +8,7 @@ import ( "github.com/vibium/clicker/internal/bidi" "github.com/vibium/clicker/internal/browser" + "github.com/vibium/clicker/internal/recording" ) // Default timeout for actionability checks @@ -27,6 +28,9 @@ type BrowserSession struct { internalCmds map[int]chan json.RawMessage // id -> response channel internalCmdsMu sync.Mutex nextInternalID int + + // Video recording + recorder *recording.Recorder } // BiDi command structure for parsing incoming messages @@ -150,6 +154,12 @@ func (r *Router) OnClientMessage(client *ClientConn, msg string) { case "vibium:find": r.handleVibiumFind(session, cmd) return + case "vibium:startRecording": + r.handleVibiumStartRecording(session, cmd) + return + case "vibium:stopRecording": + r.handleVibiumStopRecording(session, cmd) + return } // Forward standard BiDi commands to browser @@ -558,6 +568,114 @@ func (r *Router) sendInternalCommand(session *BrowserSession, method string, par } } +// handleVibiumStartRecording handles the vibium:startRecording command. +func (r *Router) handleVibiumStartRecording(session *BrowserSession, cmd bidiCommand) { + // Check if FFmpeg is available + if !recording.IsFFmpegAvailable() { + r.sendError(session, cmd.ID, fmt.Errorf("FFmpeg is not installed or not in PATH")) + return + } + + session.mu.Lock() + if session.recorder != nil { + session.mu.Unlock() + r.sendError(session, cmd.ID, fmt.Errorf("recording already in progress")) + return + } + session.mu.Unlock() + + // Parse options + fps := 10 + if fpsVal, ok := cmd.Params["fps"].(float64); ok && fpsVal > 0 { + fps = int(fpsVal) + } + + format := "mp4" + if formatVal, ok := cmd.Params["format"].(string); ok && (formatVal == "mp4" || formatVal == "webm") { + format = formatVal + } + + outputPath, _ := cmd.Params["outputPath"].(string) + + // Create screenshot function that captures from the browser + screenshotFn := func() (string, error) { + ctx, err := r.getContext(session) + if err != nil { + return "", err + } + + params := map[string]interface{}{ + "context": ctx, + } + + resp, err := r.sendInternalCommand(session, "browsingContext.captureScreenshot", params) + if err != nil { + return "", err + } + + var result struct { + Result struct { + Data string `json:"data"` + } `json:"result"` + } + if err := json.Unmarshal(resp, &result); err != nil { + return "", fmt.Errorf("failed to parse screenshot response: %w", err) + } + + return result.Result.Data, nil + } + + // Create and start recorder + recorder := recording.New(screenshotFn, recording.Options{ + FPS: fps, + OutputPath: outputPath, + Format: format, + }) + + if err := recorder.Start(); err != nil { + r.sendError(session, cmd.ID, err) + return + } + + session.mu.Lock() + session.recorder = recorder + session.mu.Unlock() + + fmt.Printf("[router] Started recording for client %d (fps=%d, format=%s)\n", session.Client.ID, fps, format) + + r.sendSuccess(session, cmd.ID, map[string]interface{}{ + "started": true, + "fps": fps, + "format": format, + }) +} + +// handleVibiumStopRecording handles the vibium:stopRecording command. +func (r *Router) handleVibiumStopRecording(session *BrowserSession, cmd bidiCommand) { + session.mu.Lock() + recorder := session.recorder + session.recorder = nil + session.mu.Unlock() + + if recorder == nil { + r.sendError(session, cmd.ID, fmt.Errorf("no recording in progress")) + return + } + + outputPath, err := recorder.Stop() + if err != nil { + r.sendError(session, cmd.ID, err) + return + } + + fmt.Printf("[router] Stopped recording for client %d, saved to: %s\n", session.Client.ID, outputPath) + + r.sendSuccess(session, cmd.ID, map[string]interface{}{ + "stopped": true, + "outputPath": outputPath, + }) +} + // closeSession closes a browser session and cleans up resources. func (r *Router) closeSession(session *BrowserSession) { session.mu.Lock() @@ -566,10 +684,19 @@ func (r *Router) closeSession(session *BrowserSession) { return } session.closed = true + recorder := session.recorder + session.recorder = nil session.mu.Unlock() fmt.Printf("[router] Closing browser session for client %d\n", session.Client.ID) + // Stop any active recording + if recorder != nil { + if outputPath, err := recorder.Stop(); err == nil { + fmt.Printf("[router] Recording saved to: %s\n", outputPath) + } + } + // Signal the routing goroutine to stop close(session.stopChan) diff --git a/clicker/internal/recording/recorder.go b/clicker/internal/recording/recorder.go new file mode 100644 index 00000000..c986cd06 --- /dev/null +++ b/clicker/internal/recording/recorder.go @@ -0,0 +1,254 @@ +package recording + +import ( + "encoding/base64" + "fmt" + "os" + "os/exec" + "path/filepath" + "sync" + "time" +) + +// ScreenshotFunc is a function that captures a screenshot and returns base64-encoded PNG. +type ScreenshotFunc func() (string, error) + +// Options configures the recorder. +type Options struct { + // FPS is the frames per second for recording. Default: 10 + FPS int + // OutputPath is where to save the video. If empty, uses temp directory. + OutputPath string + // Format is the output format: "mp4" or "webm". Default: "mp4" + Format string +} + +// Recorder captures screenshots at intervals and encodes them to video. +type Recorder struct { + opts Options + screenshotFn ScreenshotFunc + tempDir string + frameCount int + mu sync.Mutex + stopChan chan struct{} + doneChan chan struct{} + running bool + outputPath string + captureErrors []error + lastCaptureBusy bool +} + +// New creates a new Recorder. +func New(screenshotFn ScreenshotFunc, opts Options) *Recorder { + if opts.FPS <= 0 { + opts.FPS = 10 + } + if opts.Format == "" { + opts.Format = "mp4" + } + return &Recorder{ + opts: opts, + screenshotFn: screenshotFn, + } +} + +// Start begins recording. +func (r *Recorder) Start() error { + r.mu.Lock() + defer r.mu.Unlock() + + if r.running { + return fmt.Errorf("recording already in progress") + } + + // Create temp directory for frames + tempDir, err := os.MkdirTemp("", "vibium-recording-*") + if err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + r.tempDir = tempDir + r.frameCount = 0 + r.captureErrors = nil + r.stopChan = make(chan struct{}) + r.doneChan = make(chan struct{}) + r.running = true + + // Start capture goroutine + go r.captureLoop() + + return nil +} + +// Stop stops recording and encodes the video. +// Returns the path to the output video file. +func (r *Recorder) Stop() (string, error) { + r.mu.Lock() + if !r.running { + r.mu.Unlock() + return "", fmt.Errorf("no recording in progress") + } + r.running = false + close(r.stopChan) + r.mu.Unlock() + + // Wait for capture loop to finish + <-r.doneChan + + // Get frame info under lock + r.mu.Lock() + frameCount := r.frameCount + tempDir := r.tempDir + captureErrors := make([]error, len(r.captureErrors)) + copy(captureErrors, r.captureErrors) + r.mu.Unlock() + + if frameCount == 0 { + os.RemoveAll(tempDir) + return "", fmt.Errorf("no frames captured") + } + + if len(captureErrors) > 0 { + fmt.Printf("[recorder] %d capture errors occurred\n", len(captureErrors)) + } + + fmt.Printf("[recorder] Captured %d frames, encoding video...\n", frameCount) + + // Encode video + outputPath, err := r.encode() + if err != nil { + os.RemoveAll(tempDir) + return "", fmt.Errorf("failed to encode video: %w", err) + } + + // Clean up temp directory + os.RemoveAll(tempDir) + + r.outputPath = outputPath + return outputPath, nil +} + +// captureLoop captures screenshots at the configured FPS. +func (r *Recorder) captureLoop() { + defer close(r.doneChan) + + ticker := time.NewTicker(time.Second / time.Duration(r.opts.FPS)) + defer ticker.Stop() + + for { + select { + case <-r.stopChan: + return + case <-ticker.C: + r.captureFrame() + } + } +} + +// captureFrame captures a single screenshot and saves it to the temp directory. +func (r *Recorder) captureFrame() { + start := time.Now() + base64Data, err := r.screenshotFn() + elapsed := time.Since(start) + fmt.Printf("[recorder] Screenshot took %v\n", elapsed) + + if err != nil { + fmt.Printf("[recorder] Screenshot error: %v\n", err) + r.mu.Lock() + r.captureErrors = append(r.captureErrors, err) + r.mu.Unlock() + return + } + + // Decode base64 to PNG bytes + pngData, err := base64.StdEncoding.DecodeString(base64Data) + if err != nil { + fmt.Printf("[recorder] base64 decode error: %v\n", err) + r.mu.Lock() + r.captureErrors = append(r.captureErrors, err) + r.mu.Unlock() + return + } + + // FIX: Capture vars under lock ONCE, then use locally + var frameNum int + var tempDir string + r.mu.Lock() + frameNum = r.frameCount + tempDir = r.tempDir + r.frameCount++ + r.mu.Unlock() + + // Now safe to use local copies + framePath := filepath.Join(tempDir, fmt.Sprintf("frame_%06d.png", frameNum)) + fmt.Printf("[recorder] writing frame_%06d.png (%d bytes)\n", frameNum, len(pngData)) + + if err := os.WriteFile(framePath, pngData, 0644); err != nil { + fmt.Printf("[recorder] write error %s: %v\n", framePath, err) + r.mu.Lock() + r.captureErrors = append(r.captureErrors, err) + r.mu.Unlock() + } +} + +// encode uses FFmpeg to encode the captured frames to video. +func (r *Recorder) encode() (string, error) { + // Determine output path + outputPath := r.opts.OutputPath + if outputPath == "" { + ext := r.opts.Format + f, err := os.CreateTemp("", fmt.Sprintf("vibium-recording-*.%s", ext)) + if err != nil { + return "", fmt.Errorf("failed to create output file: %w", err) + } + outputPath = f.Name() + f.Close() + } + + inputPattern := filepath.Join(r.tempDir, "frame_%06d.png") + + var args []string + switch r.opts.Format { + case "webm": + args = []string{ + "-y", + "-framerate", fmt.Sprintf("%d", r.opts.FPS), + "-f", "image2", + "-i", inputPattern, + "-vf", "scale='trunc(iw/2)*2:trunc(ih/2)*2'", // Scale to even dimensions + "-c:v", "libvpx-vp9", + "-pix_fmt", "yuv420p", + "-b:v", "2M", + outputPath, + } + default: // mp4 + args = []string{ + "-y", + "-framerate", fmt.Sprintf("%d", r.opts.FPS), + "-f", "image2", + "-i", inputPattern, + "-vf", "scale='trunc(iw/2)*2:trunc(ih/2)*2'", // Scale to even dimensions + "-c:v", "libx264", + "-pix_fmt", "yuv420p", + "-preset", "fast", + "-crf", "23", + outputPath, + } + } + + fmt.Printf("[recorder] running ffmpeg %v\n", args) + cmd := exec.Command("ffmpeg", args...) + output, err := cmd.CombinedOutput() + fmt.Printf("[recorder] ffmpeg output:\n%s\n", string(output)) + + if err != nil { + return "", fmt.Errorf("ffmpeg failed: %w\nOutput: %s", err, string(output)) + } + + return outputPath, nil +} + +// IsFFmpegAvailable checks if FFmpeg is installed and available. +func IsFFmpegAvailable() bool { + cmd := exec.Command("ffmpeg", "-version") + return cmd.Run() == nil +} diff --git a/clients/javascript/package-lock.json b/clients/javascript/package-lock.json new file mode 100644 index 00000000..bd00acb0 --- /dev/null +++ b/clients/javascript/package-lock.json @@ -0,0 +1,1525 @@ +{ + "name": "vibium-client", + "version": "0.1.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vibium-client", + "version": "0.1.2", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/ws": "^8.18.1", + "tsup": "^8.0.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/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/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "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.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", + "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/clients/javascript/src/index.ts b/clients/javascript/src/index.ts index b7946b45..c19e4044 100644 --- a/clients/javascript/src/index.ts +++ b/clients/javascript/src/index.ts @@ -1,5 +1,5 @@ export { browser } from './browser'; -export { Vibe, FindOptions } from './vibe'; +export { Vibe, FindOptions, RecordingOptions } from './vibe'; export { Element, BoundingBox, ElementInfo, ActionOptions } from './element'; // Sync API diff --git a/clients/javascript/src/sync/vibe.ts b/clients/javascript/src/sync/vibe.ts index 0dc2a9ce..de00fb20 100644 --- a/clients/javascript/src/sync/vibe.ts +++ b/clients/javascript/src/sync/vibe.ts @@ -1,7 +1,7 @@ import { SyncBridge } from './bridge'; import { ElementSync } from './element'; import { ElementInfo } from '../element'; -import { FindOptions } from '../vibe'; +import { FindOptions, RecordingOptions } from '../vibe'; export class VibeSync { private bridge: SyncBridge; @@ -36,6 +36,23 @@ export class VibeSync { return new ElementSync(this.bridge, result.elementId, result.info); } + /** + * Start recording the browser session as a video. + * Requires FFmpeg to be installed on the system. + */ + startRecording(options?: RecordingOptions): void { + this.bridge.call('startRecording', [options]); + } + + /** + * Stop recording and save the video file. + * @returns Path to the saved video file + */ + stopRecording(): string { + const result = this.bridge.call<{ outputPath: string }>('stopRecording'); + return result.outputPath; + } + quit(): void { this.bridge.call('quit'); this.bridge.terminate(); diff --git a/clients/javascript/src/sync/worker.ts b/clients/javascript/src/sync/worker.ts index e2a66742..13db61c9 100644 --- a/clients/javascript/src/sync/worker.ts +++ b/clients/javascript/src/sync/worker.ts @@ -1,6 +1,6 @@ import { parentPort, workerData } from 'worker_threads'; import { browser } from '../browser'; -import { Vibe, FindOptions } from '../vibe'; +import { Vibe, FindOptions, RecordingOptions } from '../vibe'; import { Element, ActionOptions } from '../element'; interface WorkerData { @@ -96,6 +96,19 @@ async function handleCommand(cmd: Command): Promise { return { box }; } + case 'startRecording': { + if (!vibe) throw new Error('Browser not launched'); + const [options] = cmd.args as [RecordingOptions | undefined]; + await vibe.startRecording(options); + return { success: true }; + } + + case 'stopRecording': { + if (!vibe) throw new Error('Browser not launched'); + const outputPath = await vibe.stopRecording(); + return { outputPath }; + } + case 'quit': { if (!vibe) throw new Error('Browser not launched'); await vibe.quit(); diff --git a/clients/javascript/src/vibe.ts b/clients/javascript/src/vibe.ts index 76afe56a..3e0edc99 100644 --- a/clients/javascript/src/vibe.ts +++ b/clients/javascript/src/vibe.ts @@ -8,6 +8,26 @@ export interface FindOptions { timeout?: number; } +export interface RecordingOptions { + /** Frames per second. Default: 10 */ + fps?: number; + /** Output format: 'mp4' or 'webm'. Default: 'mp4' */ + format?: 'mp4' | 'webm'; + /** Output file path. If not provided, uses temp directory */ + outputPath?: string; +} + +interface StartRecordingResult { + started: boolean; + fps: number; + format: string; +} + +interface StopRecordingResult { + stopped: boolean; + outputPath: string; +} + interface VibiumFindResult { tag: string; text: string; @@ -105,6 +125,57 @@ export class Vibe { return new Element(this.client, context, selector, info); } + /** + * Start recording the browser session as a video. + * Requires FFmpeg to be installed on the system. + * + * @param options - Recording options (fps, format, outputPath) + * @returns Promise that resolves when recording starts + * + * @example + * ```typescript + * await vibe.startRecording({ fps: 10, format: 'mp4' }); + * // ... perform actions ... + * const videoPath = await vibe.stopRecording(); + * ``` + */ + async startRecording(options?: RecordingOptions): Promise { + debug('starting recording', { fps: options?.fps, format: options?.format, outputPath: options?.outputPath }); + const context = await this.getContext(); + + await this.client.send('vibium:startRecording', { + context, + fps: options?.fps, + format: options?.format, + outputPath: options?.outputPath, + }); + + debug('recording started'); + } + + /** + * Stop recording and save the video file. + * + * @returns Promise that resolves with the path to the saved video file + * + * @example + * ```typescript + * const videoPath = await vibe.stopRecording(); + * console.log('Video saved to:', videoPath); + * ``` + */ + async stopRecording(): Promise { + debug('stopping recording'); + const context = await this.getContext(); + + const result = await this.client.send('vibium:stopRecording', { + context, + }); + + debug('recording stopped', { outputPath: result.outputPath }); + return result.outputPath; + } + async quit(): Promise { await this.client.close(); if (this.process) { diff --git a/clients/python/src/vibium/__init__.py b/clients/python/src/vibium/__init__.py index e9c09322..c1719f3f 100644 --- a/clients/python/src/vibium/__init__.py +++ b/clients/python/src/vibium/__init__.py @@ -2,6 +2,7 @@ from .browser import browser from .browser_sync import browser_sync +from .vibe import RecordingOptions __version__ = "0.1.0" -__all__ = ["browser", "browser_sync"] +__all__ = ["browser", "browser_sync", "RecordingOptions"] diff --git a/clients/python/src/vibium/browser_sync.py b/clients/python/src/vibium/browser_sync.py index e2a1c2af..6493c0be 100644 --- a/clients/python/src/vibium/browser_sync.py +++ b/clients/python/src/vibium/browser_sync.py @@ -2,7 +2,7 @@ import asyncio import threading -from typing import Optional +from typing import Literal, Optional from .browser import browser from .element import Element, ElementInfo @@ -93,6 +93,33 @@ def find(self, selector: str, timeout: Optional[int] = None) -> ElementSync: element = self._loop_thread.run(self._vibe.find(selector, timeout)) return ElementSync(element, self._loop_thread) + def start_recording( + self, + fps: int = 10, + format: Literal["mp4", "webm"] = "mp4", + output_path: Optional[str] = None, + ) -> None: + """Start recording the browser session as a video. + + Requires FFmpeg to be installed on the system. + + Args: + fps: Frames per second. Default: 10 + format: Output format ('mp4' or 'webm'). Default: 'mp4' + output_path: Output file path. If not provided, uses temp directory. + """ + self._loop_thread.run( + self._vibe.start_recording(fps=fps, format=format, output_path=output_path) + ) + + def stop_recording(self) -> str: + """Stop recording and save the video file. + + Returns: + Path to the saved video file. + """ + return self._loop_thread.run(self._vibe.stop_recording()) + def quit(self) -> None: """Close the browser and clean up resources.""" self._loop_thread.run(self._vibe.quit()) diff --git a/clients/python/src/vibium/vibe.py b/clients/python/src/vibium/vibe.py index 9a4fb781..e5f27552 100644 --- a/clients/python/src/vibium/vibe.py +++ b/clients/python/src/vibium/vibe.py @@ -1,13 +1,28 @@ """Vibe class - the main browser automation interface.""" import base64 -from typing import Optional +from dataclasses import dataclass +from typing import Literal, Optional from .client import BiDiClient from .clicker import ClickerProcess from .element import BoundingBox, Element, ElementInfo +@dataclass +class RecordingOptions: + """Options for video recording.""" + + fps: int = 10 + """Frames per second. Default: 10""" + + format: Literal["mp4", "webm"] = "mp4" + """Output format. Default: 'mp4'""" + + output_path: Optional[str] = None + """Output file path. If not provided, uses temp directory.""" + + class Vibe: """Main browser automation interface. @@ -98,6 +113,56 @@ async def find(self, selector: str, timeout: Optional[int] = None) -> Element: return Element(self._client, context, selector, info) + async def start_recording( + self, + fps: int = 10, + format: Literal["mp4", "webm"] = "mp4", + output_path: Optional[str] = None, + ) -> None: + """Start recording the browser session as a video. + + Requires FFmpeg to be installed on the system. + + Args: + fps: Frames per second. Default: 10 + format: Output format ('mp4' or 'webm'). Default: 'mp4' + output_path: Output file path. If not provided, uses temp directory. + + Example: + >>> await vibe.start_recording(fps=10, format='mp4') + >>> # ... perform actions ... + >>> video_path = await vibe.stop_recording() + """ + context = await self._get_context() + + params = { + "context": context, + "fps": fps, + "format": format, + } + if output_path is not None: + params["outputPath"] = output_path + + await self._client.send("vibium:startRecording", params) + + async def stop_recording(self) -> str: + """Stop recording and save the video file. + + Returns: + Path to the saved video file. + + Example: + >>> video_path = await vibe.stop_recording() + >>> print(f"Video saved to: {video_path}") + """ + context = await self._get_context() + + result = await self._client.send( + "vibium:stopRecording", + {"context": context}, + ) + return result["outputPath"] + async def quit(self) -> None: """Close the browser and clean up resources.""" await self._client.close() diff --git a/tests/js/recording.test.js b/tests/js/recording.test.js new file mode 100644 index 00000000..499fbe6d --- /dev/null +++ b/tests/js/recording.test.js @@ -0,0 +1,138 @@ +/** + * JS Library Tests: Video Recording + * Tests startRecording() and stopRecording() methods + * + * Note: These tests require FFmpeg to be installed on the system. + * If FFmpeg is not available, tests will be skipped. + */ + +const { test, describe, before } = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); +const { execSync } = require('node:child_process'); + +// Import from built library +const { browser } = require('../../clients/javascript/dist'); + +// Check if FFmpeg is available +function isFFmpegAvailable() { + try { + execSync('ffmpeg -version', { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +const skipReason = isFFmpegAvailable() ? null : 'FFmpeg not available'; + +describe('JS Recording API', { skip: skipReason }, () => { + test('startRecording() and stopRecording() create video file', async () => { + const vibe = await browser.launch({ headless: true }); + try { + await vibe.go('https://the-internet.herokuapp.com/'); + + // Start recording + await vibe.startRecording({ fps: 5, format: 'mp4' }); + + // Wait a bit and navigate to capture some frames + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Navigate to another page + await vibe.go('https://the-internet.herokuapp.com/add_remove_elements/'); + + // Wait a bit more + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Stop recording + const videoPath = await vibe.stopRecording(); + + // Verify video was created + assert.ok(videoPath, 'Should return video path'); + assert.ok(fs.existsSync(videoPath), 'Video file should exist'); + + // Check file has content + const stats = fs.statSync(videoPath); + assert.ok(stats.size > 1000, 'Video file should have reasonable size'); + + // Clean up + fs.unlinkSync(videoPath); + } finally { + await vibe.quit(); + } + }); + + test('startRecording() with custom output path', async () => { + const outputPath = path.join(__dirname, 'test-recording.mp4'); + + // Clean up any previous test file + if (fs.existsSync(outputPath)) { + fs.unlinkSync(outputPath); + } + + const vibe = await browser.launch({ headless: true }); + try { + await vibe.go('https://the-internet.herokuapp.com/'); + + // Start recording with custom path + await vibe.startRecording({ fps: 5, format: 'mp4', outputPath }); + + // Wait to capture frames + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Stop recording + const returnedPath = await vibe.stopRecording(); + + // Verify path matches + assert.strictEqual(returnedPath, outputPath, 'Should return specified output path'); + assert.ok(fs.existsSync(outputPath), 'Video file should exist at specified path'); + + // Clean up + fs.unlinkSync(outputPath); + } finally { + await vibe.quit(); + } + }); + + test('stopRecording() without starting throws error', async () => { + const vibe = await browser.launch({ headless: true }); + try { + await vibe.go('https://the-internet.herokuapp.com/'); + + // Try to stop without starting + await assert.rejects( + async () => await vibe.stopRecording(), + /no recording in progress/i, + 'Should throw error when no recording is in progress' + ); + } finally { + await vibe.quit(); + } + }); + + test('startRecording() twice throws error', async () => { + const vibe = await browser.launch({ headless: true }); + try { + await vibe.go('https://the-internet.herokuapp.com/'); + + // Start first recording + await vibe.startRecording({ fps: 5 }); + + // Try to start another + await assert.rejects( + async () => await vibe.startRecording({ fps: 5 }), + /recording already in progress/i, + 'Should throw error when recording already in progress' + ); + + // Clean up: stop the first recording + const videoPath = await vibe.stopRecording(); + if (fs.existsSync(videoPath)) { + fs.unlinkSync(videoPath); + } + } finally { + await vibe.quit(); + } + }); +});