diff --git a/apps/ios-playground/README.md b/apps/ios-playground/README.md new file mode 100644 index 000000000..da0aed396 --- /dev/null +++ b/apps/ios-playground/README.md @@ -0,0 +1,66 @@ +# Midscene iOS Playground + +A playground for testing Midscene iOS automation features with automatic device mirroring setup. + +See https://midscenejs.com/ for details. + +## Features + +### ✨ Auto-Detection of iPhone Mirroring + +The playground can automatically detect and configure the iPhone Mirroring app window: + +1. **Automatic Setup**: When you connect, the playground automatically tries to detect your iPhone Mirroring window +2. **Smart Configuration**: It calculates the optimal screen mapping based on window size and device type +3. **Manual Override**: If auto-detection doesn't work, you can manually configure the mirror settings + +### 🎯 How Auto-Detection Works + +1. **Window Detection**: Uses AppleScript to find the iPhone Mirroring app window +2. **Content Area Calculation**: Automatically calculates the device screen area within the window (excluding title bars and padding) +3. **Device Matching**: Matches the aspect ratio to common iOS devices for optimal coordinate mapping +4. **Instant Configuration**: Sets up the coordinate transformation automatically + +## Usage + +### Prerequisites + +1. **macOS with iPhone Mirroring**: Ensure iPhone Mirroring is available and working +2. **iOS Device**: Connected and mirroring to your Mac +3. **Python Server**: The PyAutoGUI server running on port 1412 + +### Quick Start + +1. **Start the server**: + ```bash + cd packages/ios/idb + python auto_server.py + ``` + +2. **Launch the playground**: + ```bash + npm run dev + ``` + +3. **Open iPhone Mirroring app** on your Mac + +4. **Auto-configure**: Click "Auto Detect" to automatically set up the mirroring coordinates + +### UI Controls + +- **📷 Screenshot**: Take a screenshot of the configured iOS device area +- **🔍 Auto Detect**: Automatically detect and configure iPhone Mirroring window +- **⚙️ Manual Config**: Manually set mirror window coordinates + +## Troubleshooting + +### Auto-Detection Issues + +1. **"iPhone Mirroring app not found"**: Make sure iPhone Mirroring app is open and visible +2. **"Window seems too small"**: Try resizing the iPhone Mirroring window to be larger +3. **Coordinates seem wrong**: Use manual configuration to fine-tune the coordinates + +### Server Connection Issues + +1. **Server not responding**: Check if server is running on port 1412 +2. **Permission issues**: Ensure macOS accessibility permissions are granted to Terminal/Python diff --git a/apps/ios-playground/package.json b/apps/ios-playground/package.json new file mode 100644 index 000000000..50c8ab069 --- /dev/null +++ b/apps/ios-playground/package.json @@ -0,0 +1,35 @@ +{ + "name": "ios-playground", + "private": true, + "version": "0.12.4", + "type": "module", + "scripts": { + "build": "rsbuild build", + "dev": "rsbuild dev --open", + "preview": "rsbuild preview" + }, + "dependencies": { + "@ant-design/icons": "^5.3.1", + "@midscene/ios": "workspace:*", + "@midscene/core": "workspace:*", + "@midscene/shared": "workspace:*", + "@midscene/visualizer": "workspace:*", + "@midscene/web": "workspace:*", + "antd": "^5.21.6", + "dayjs": "^1.11.11", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@rsbuild/core": "^1.3.22", + "@rsbuild/plugin-less": "^1.2.4", + "@rsbuild/plugin-node-polyfill": "1.3.0", + "@rsbuild/plugin-react": "^1.3.1", + "@rsbuild/plugin-svgr": "^1.1.1", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "archiver": "^6.0.0", + "less": "^4.2.0", + "typescript": "^5.8.3" + } +} diff --git a/apps/ios-playground/rsbuild.config.ts b/apps/ios-playground/rsbuild.config.ts new file mode 100644 index 000000000..1449447f1 --- /dev/null +++ b/apps/ios-playground/rsbuild.config.ts @@ -0,0 +1,88 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { defineConfig } from '@rsbuild/core'; +import { pluginLess } from '@rsbuild/plugin-less'; +import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'; +import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginSvgr } from '@rsbuild/plugin-svgr'; +import { pluginTypeCheck } from '@rsbuild/plugin-type-check'; + +const copyAndroidPlaygroundStatic = () => ({ + name: 'copy-android-playground-static', + setup(api) { + api.onAfterBuild(async () => { + const srcDir = path.join(__dirname, 'dist'); + const destDir = path.join( + __dirname, + '..', + '..', + 'packages', + 'android-playground', + 'static', + ); + const faviconSrc = path.join(__dirname, 'src', 'favicon.ico'); + const faviconDest = path.join(destDir, 'favicon.ico'); + + await fs.promises.mkdir(destDir, { recursive: true }); + // Copy directory contents recursively + await fs.promises.cp(srcDir, destDir, { recursive: true }); + // Copy favicon + await fs.promises.copyFile(faviconSrc, faviconDest); + + console.log(`Copied build artifacts to ${destDir}`); + console.log(`Copied favicon to ${faviconDest}`); + }); + }, +}); + +export default defineConfig({ + environments: { + web: { + source: { + entry: { + index: './src/index.tsx', + }, + }, + output: { + target: 'web', + sourceMap: true, + }, + html: { + title: 'Midscene iOS Playground', + }, + }, + }, + dev: { + writeToDisk: true, + }, + server: { + proxy: { + '/api/pyautogui': { + target: 'http://localhost:1412', + changeOrigin: true, + pathRewrite: { + '^/api/pyautogui': '' + } + } + } + }, + resolve: { + alias: { + async_hooks: path.join(__dirname, './src/scripts/blank_polyfill.ts'), + 'node:async_hooks': path.join( + __dirname, + './src/scripts/blank_polyfill.ts', + ), + react: path.resolve(__dirname, 'node_modules/react'), + 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), + }, + }, + plugins: [ + pluginReact(), + pluginNodePolyfill(), + pluginLess(), + pluginSvgr(), + copyAndroidPlaygroundStatic(), + pluginTypeCheck(), + ], +}); diff --git a/apps/ios-playground/src/App.less b/apps/ios-playground/src/App.less new file mode 100644 index 000000000..f01312f66 --- /dev/null +++ b/apps/ios-playground/src/App.less @@ -0,0 +1,183 @@ +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', + Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; + font-size: 14px; +} + +.app-container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +.app-content { + height: 100vh; + overflow: hidden; +} + +.app-grid-layout { + height: 100%; + display: flex; + + .ant-row { + flex: 1; + width: 100%; + height: 100%; + display: flex; + flex-wrap: nowrap; + width: 100%; + } +} + +.app-panel { + height: 100%; + background-color: #fff; + border-radius: 0; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + transition: box-shadow 0.3s; + overflow: hidden; + + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); + } + + &.left-panel { + width: 480px; + flex: none; + overflow: hidden; + height: 100%; + display: flex; + flex-direction: column; + } + + &.right-panel { + border-radius: 0; + flex: 1; + overflow: hidden; + box-shadow: -4px 0px 20px 0px #0000000A; + } +} + +.panel-content { + padding: 12px 24px 24px 24px; + height: 100%; + overflow: auto; + display: flex; + flex-direction: column; + border-left: 1px solid rgba(0, 0, 0, 0.08); + + &.left-panel-content { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + + &.right-panel-content { + border-radius: 0; + } + + h2 { + color: #000; + font-size: 18px; + margin-top: 16px; + margin-bottom: 12px; + } + + canvas { + max-width: 100%; + margin-top: 16px; + border: 1px solid #f0f0f0; + border-radius: 4px; + } +} + +.command-form { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + + .form-content { + display: flex; + flex-direction: column; + height: 100%; + gap: 24px; + } + + .command-input-wrapper { + margin-top: 8px; + } +} + +.result-container { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; + position: relative; + height: 100%; +} + +@media (max-width: 768px) { + .app-container { + height: auto; + min-height: 100vh; + } + + .app-grid-layout .ant-row { + flex-wrap: wrap !important; + } + + .app-panel { + margin-bottom: 16px; + height: auto; + min-height: 200px; + width: 100% !important; + flex: 0 0 100% !important; + + &:first-child { + border-radius: 20px; + + .panel-content { + border-radius: 20px; + } + } + } + + .panel-content { + padding: 12px; + + h2 { + font-size: 16px; + margin-bottom: 12px; + padding-bottom: 6px; + } + + textarea { + min-height: 100px; + } + } +} + +@media (min-width: 769px) and (max-width: 992px) { + .app-panel { + margin-bottom: 16px; + min-height: 300px; + } +} + +.resize-handle { + width: 2px; + background-color: #f0f0f0; + transition: background-color 0.2s; + + &:hover { + background-color: #1677ff; + } +} \ No newline at end of file diff --git a/apps/ios-playground/src/App.tsx b/apps/ios-playground/src/App.tsx new file mode 100644 index 000000000..d44e74a28 --- /dev/null +++ b/apps/ios-playground/src/App.tsx @@ -0,0 +1,276 @@ +import './App.less'; +import { overrideAIConfig } from '@midscene/shared/env'; +import { + EnvConfig, + Logo, + type PlaygroundResult, + PlaygroundResultView, + PromptInput, + type ReplayScriptsInfo, + allScriptsFromDump, + cancelTask, + getTaskProgress, + globalThemeConfig, + overrideServerConfig, + requestPlaygroundServer, + useEnvConfig, + useServerValid, +} from '@midscene/visualizer'; +import { Col, ConfigProvider, Form, Layout, Row, message } from 'antd'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import IOSPlayer, { type IOSPlayerRefMethods } from './ios-player'; + +import './ios-device/index.less'; + +const { Content } = Layout; + +export default function App() { + const [form] = Form.useForm(); + const selectedType = Form.useWatch('type', form); + const [loading, setLoading] = useState(false); + const [messageApi, contextHolder] = message.useMessage(); + const [result, setResult] = useState({ + result: undefined, + dump: null, + reportHTML: null, + error: null, + }); + const [replayCounter, setReplayCounter] = useState(0); + const [replayScriptsInfo, setReplayScriptsInfo] = + useState(null); + const { config, deepThink } = useEnvConfig(); + const [loadingProgressText, setLoadingProgressText] = useState(''); + const currentRequestIdRef = useRef(null); + const pollIntervalRef = useRef | null>(null); + const configAlreadySet = Object.keys(config || {}).length >= 1; + const serverValid = useServerValid(true); + + // iOS Player ref + const iosPlayerRef = useRef(null); + + // clear the polling interval + const clearPollingInterval = useCallback(() => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + }, []); + + // start polling task progress + const startPollingProgress = useCallback( + (requestId: string) => { + clearPollingInterval(); + + // set polling interval to 500ms + pollIntervalRef.current = setInterval(async () => { + try { + const data = await getTaskProgress(requestId); + + if (data.tip) { + setLoadingProgressText(data.tip); + } + } catch (error) { + console.error('Failed to poll task progress:', error); + } + }, 500); + }, + [clearPollingInterval], + ); + + // clean up the polling when the component unmounts + useEffect(() => { + return () => { + clearPollingInterval(); + }; + }, [clearPollingInterval]); + + // Override AI configuration + useEffect(() => { + overrideAIConfig(config); + overrideServerConfig(config); + }, [config]); + + // handle run button click + const handleRun = useCallback(async () => { + if (!serverValid) { + messageApi.warning( + 'Playground server is not ready, please try again later', + ); + return; + } + + setLoading(true); + setResult(null); + setReplayScriptsInfo(null); + setLoadingProgressText(''); + + const { type, prompt } = form.getFieldsValue(); + + const thisRunningId = Date.now().toString(); + + currentRequestIdRef.current = thisRunningId; + + // start polling progress immediately + startPollingProgress(thisRunningId); + + try { + // Use a fixed context string for iOS since we don't have device selection + const res = await requestPlaygroundServer( + 'ios-device', + type, + prompt, + { + requestId: thisRunningId, + deepThink, + }, + ); + + // stop polling + clearPollingInterval(); + + setResult(res); + setLoading(false); + + if (!res) { + throw new Error('server returned empty response'); + } + + // handle the special case of aiAction type, extract script information + if (res?.dump && !['aiQuery', 'aiAssert'].includes(type)) { + const info = allScriptsFromDump(res.dump); + setReplayScriptsInfo(info); + setReplayCounter((c) => c + 1); + } else { + setReplayScriptsInfo(null); + } + messageApi.success('Command executed'); + + } catch (error) { + clearPollingInterval(); + setLoading(false); + console.error('execute command error:', error); + messageApi.error( + `Command execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } + }, [ + messageApi, + serverValid, + form, + startPollingProgress, + clearPollingInterval, + deepThink, + ]); + + const resetResult = () => { + setResult(null); + setReplayScriptsInfo(null); + setLoading(false); + }; + + // handle stop button click + const handleStop = useCallback(async () => { + clearPollingInterval(); + setLoading(false); + resetResult(); + if (currentRequestIdRef.current) { + await cancelTask(currentRequestIdRef.current); + } + messageApi.info('Operation stopped'); + }, [messageApi, clearPollingInterval]); + + return ( + + {contextHolder} + + +
+ + {/* left panel: PromptInput */} + +
+
+ + +
+

Command input

+
+
+
+ +
+
+ + Don't worry, just one more step to launch the + playground server. +
+ + npx --yes @midscene/ios-playground + +
+ And make sure PyAutoGUI server is running on port 1412 + + } + /> +
+
+
+
+ + + {/* right panel: IOSPlayer */} + +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/apps/ios-playground/src/env.d.ts b/apps/ios-playground/src/env.d.ts new file mode 100644 index 000000000..6fb53468e --- /dev/null +++ b/apps/ios-playground/src/env.d.ts @@ -0,0 +1,10 @@ +/// + +declare module '*.svg' { + const content: string; + export default content; +} +declare module '*.svg?react' { + const ReactComponent: React.FunctionComponent>; + export default ReactComponent; +} diff --git a/apps/ios-playground/src/favicon.ico b/apps/ios-playground/src/favicon.ico new file mode 100644 index 000000000..3780090a9 Binary files /dev/null and b/apps/ios-playground/src/favicon.ico differ diff --git a/apps/ios-playground/src/icons/linked.svg b/apps/ios-playground/src/icons/linked.svg new file mode 100644 index 000000000..6849e852f --- /dev/null +++ b/apps/ios-playground/src/icons/linked.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/ios-playground/src/icons/screenshot.svg b/apps/ios-playground/src/icons/screenshot.svg new file mode 100644 index 000000000..8b477bb98 --- /dev/null +++ b/apps/ios-playground/src/icons/screenshot.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/ios-playground/src/icons/unlink.svg b/apps/ios-playground/src/icons/unlink.svg new file mode 100644 index 000000000..1529f0a62 --- /dev/null +++ b/apps/ios-playground/src/icons/unlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/ios-playground/src/index.tsx b/apps/ios-playground/src/index.tsx new file mode 100644 index 000000000..2c2eb681b --- /dev/null +++ b/apps/ios-playground/src/index.tsx @@ -0,0 +1,8 @@ +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootEl = document.getElementById('root'); +if (rootEl) { + const root = ReactDOM.createRoot(rootEl); + root.render(); +} diff --git a/apps/ios-playground/src/ios-device/index.less b/apps/ios-playground/src/ios-device/index.less new file mode 100644 index 000000000..8b3e6a70d --- /dev/null +++ b/apps/ios-playground/src/ios-device/index.less @@ -0,0 +1,44 @@ +.ios-device-container { + .status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + + &.connected { + background-color: #52c41a; + } + + &.disconnected { + background-color: #ff4d4f; + } + } + + .connection-actions { + border: 1px dashed #d9d9d9; + border-radius: 6px; + padding: 12px; + background-color: #fafafa; + } + + .connection-info { + text-align: center; + padding: 8px; + background-color: #f6ffed; + border: 1px solid #b7eb8f; + border-radius: 6px; + } + + .server-status { + padding: 8px 12px; + background-color: #fafafa; + border-radius: 6px; + border: 1px solid #d9d9d9; + } + + .server-url { + padding: 8px 12px; + background-color: #f0f0f0; + border-radius: 6px; + } +} diff --git a/apps/ios-playground/src/ios-device/index.tsx b/apps/ios-playground/src/ios-device/index.tsx new file mode 100644 index 000000000..6fcb4eff1 --- /dev/null +++ b/apps/ios-playground/src/ios-device/index.tsx @@ -0,0 +1,129 @@ +import { Button, Card, Space, Typography } from 'antd'; +import { useEffect, useState } from 'react'; +import './index.less'; + +const { Text, Title } = Typography; + +interface IOSDeviceProps { + serverUrl?: string; + onServerStatusChange?: (connected: boolean) => void; +} + +export default function IOSDevice({ + serverUrl = 'http://localhost:1412', + onServerStatusChange, +}: IOSDeviceProps) { + const [serverConnected, setServerConnected] = useState(false); + const [checking, setChecking] = useState(false); + + // Helper function to get the appropriate URL for API calls + const getApiUrl = (endpoint: string) => { + // In development, use proxy; in production or when server is not localhost:1412, use direct URL + if (serverUrl === 'http://localhost:1412' && process.env.NODE_ENV === 'development') { + return `/api/pyautogui${endpoint}`; + } + return `${serverUrl}${endpoint}`; + }; + + const checkServerStatus = async () => { + setChecking(true); + try { + // Use proxy endpoint to avoid CORS issues + const response = await fetch(getApiUrl('/health')); + const connected = response.ok; + setServerConnected(connected); + onServerStatusChange?.(connected); + } catch (error) { + console.error('Failed to check server status:', error); + setServerConnected(false); + onServerStatusChange?.(false); + } finally { + setChecking(false); + } + }; + + const startPyAutoGUIServer = () => { + // Show instructions to user since we can't start server from frontend + const message = `Please start the PyAutoGUI server manually: + +1. Open Terminal +2. Run: npx @midscene/ios server +3. Make sure iPhone Mirroring app is open and connected`; + + alert(message); + }; + + useEffect(() => { + checkServerStatus(); + // Check server status every 3 seconds + const interval = setInterval(checkServerStatus, 3000); + return () => clearInterval(interval); + }, [serverUrl]); + + return ( +
+ + + iOS Device Connection + + + } + size="small" + > + +
+ + PyAutoGUI Server: +
+ + {serverConnected ? 'Connected' : 'Disconnected'} + + +
+ +
+ + Server URL: + {serverUrl} + +
+ + {!serverConnected && ( +
+ + + + +
+ )} + + {serverConnected && ( +
+ + ✅ Ready for iOS automation + +
+ )} +
+ +
+ ); +} diff --git a/apps/ios-playground/src/ios-player/index.less b/apps/ios-playground/src/ios-player/index.less new file mode 100644 index 000000000..2c3f9927a --- /dev/null +++ b/apps/ios-playground/src/ios-player/index.less @@ -0,0 +1,60 @@ +.ios-player-container { + .mirror-config { + margin-bottom: 16px; + padding: 12px; + background-color: #fafafa; + border: 1px solid #d9d9d9; + border-radius: 6px; + } + + .config-status { + margin-bottom: 12px; + padding: 8px; + border-radius: 4px; + + &.enabled { + background-color: #f6ffed; + border: 1px solid #b7eb8f; + color: #52c41a; + } + + &.disabled { + background-color: #fff7e6; + border: 1px solid #ffd591; + color: #fa8c16; + } + } + + .display-area { + min-height: 400px; + display: flex; + align-items: center; + justify-content: center; + background-color: #f5f5f5; + border: 1px solid #d9d9d9; + border-radius: 6px; + position: relative; + + .placeholder { + text-align: center; + color: #8c8c8c; + } + + .screenshot-container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + + .ios-screenshot { + max-width: 100%; + max-height: 500px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid #d9d9d9; + } + } + } +} diff --git a/apps/ios-playground/src/ios-player/index.tsx b/apps/ios-playground/src/ios-player/index.tsx new file mode 100644 index 000000000..a03431f50 --- /dev/null +++ b/apps/ios-playground/src/ios-player/index.tsx @@ -0,0 +1,171 @@ +import { Card, Button, Space, Typography, message, Tooltip } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; +import './index.less'; + +const { Text } = Typography; + +export interface IOSPlayerRefMethods { + refreshDisplay: () => Promise; +} + +interface IOSPlayerProps { + serverUrl?: string; + autoConnect?: boolean; +} + +const IOSPlayer = forwardRef( + ({ serverUrl = 'http://localhost:1412', autoConnect = false }, ref) => { + const [connected, setConnected] = useState(false); + const [autoDetecting, setAutoDetecting] = useState(false); + const [messageApi, contextHolder] = message.useMessage(); + const [mirrorConfig, setMirrorConfig] = useState(null); + + // Helper function to get the appropriate URL for API calls + const getApiUrl = (endpoint: string) => { + // In development, use proxy; in production or when server is not localhost:1412, use direct URL + if (serverUrl === 'http://localhost:1412' && process.env.NODE_ENV === 'development') { + return `/api/pyautogui${endpoint}`; + } + return `${serverUrl}${endpoint}`; + }; + + const checkConnection = async () => { + try { + const response = await fetch(getApiUrl('/health')); + const isConnected = response.ok; + setConnected(isConnected); + + // If connected, also get the current config + if (isConnected) { + try { + const configResponse = await fetch(getApiUrl('/config')); + const configResult = await configResponse.json(); + if (configResult.status === 'ok') { + setMirrorConfig(configResult.config); + } + } catch (error) { + // Ignore config fetch errors + console.warn('Failed to fetch config:', error); + } + } + + return isConnected; + } catch (error) { + setConnected(false); + setMirrorConfig(null); + return false; + } + }; + + const autoDetectMirror = async () => { + if (!connected) { + messageApi.warning('Server is not connected'); + return; + } + + setAutoDetecting(true); + try { + const response = await fetch(getApiUrl('/detect'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + + const result = await response.json(); + if (result.status === 'ok') { + messageApi.success(`Auto-configured: ${result.message}`); + setMirrorConfig(result.config); + } else { + messageApi.error(`Auto-detection failed: ${result.error}`); + if (result.suggestion) { + messageApi.info(result.suggestion); + } + } + } catch (error) { + messageApi.error(`Auto-detection error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { + setAutoDetecting(false); + } + }; + + useImperativeHandle(ref, () => ({ + refreshDisplay: async () => { + // Just refresh the connection status + await checkConnection(); + }, + })); + + useEffect(() => { + checkConnection(); + const interval = setInterval(checkConnection, 3000); + return () => clearInterval(interval); + }, [serverUrl]); + + useEffect(() => { + if (autoConnect && connected) { + // Try auto-detection when connected + autoDetectMirror(); + } + }, [autoConnect, connected]); + + return ( +
+ {contextHolder} + + iOS Display + {connected && ( + + + + + + )} + + } + size="small" + > + {connected && mirrorConfig && mirrorConfig.enabled && ( +
+ + ✅ Configured: {mirrorConfig.estimated_ios_width}×{mirrorConfig.estimated_ios_height} device + → {mirrorConfig.mirror_width}×{mirrorConfig.mirror_height} at ({mirrorConfig.mirror_x}, {mirrorConfig.mirror_y}) + +
+ )} + +
+ {!connected ? ( +
+ + Waiting for iOS device connection... +
+ Please ensure iPhone Mirroring is active +
+
+ ) : ( +
+ + iOS device connected. Use Auto Detect to configure mirroring. + +
+ )} +
+
+
+ ); + } +); + +IOSPlayer.displayName = 'IOSPlayer'; + +export default IOSPlayer; diff --git a/apps/ios-playground/src/scripts/blank_polyfill.ts b/apps/ios-playground/src/scripts/blank_polyfill.ts new file mode 100644 index 000000000..d1eb7e91a --- /dev/null +++ b/apps/ios-playground/src/scripts/blank_polyfill.ts @@ -0,0 +1,2 @@ +const AsyncLocalStorage = {}; +export { AsyncLocalStorage }; \ No newline at end of file diff --git a/apps/ios-playground/tsconfig.json b/apps/ios-playground/tsconfig.json new file mode 100644 index 000000000..ca9a7367e --- /dev/null +++ b/apps/ios-playground/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "jsx": "react-jsx", + "target": "ES2020", + "skipLibCheck": true, + "useDefineForClassFields": true, + + /* modules */ + "module": "ESNext", + "isolatedModules": true, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + + /* type checking */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/apps/site/docs/en/automate-with-scripts-in-yaml.mdx b/apps/site/docs/en/automate-with-scripts-in-yaml.mdx index 6f51625f0..3e6d7b718 100644 --- a/apps/site/docs/en/automate-with-scripts-in-yaml.mdx +++ b/apps/site/docs/en/automate-with-scripts-in-yaml.mdx @@ -80,6 +80,27 @@ tasks: - aiAssert: The results show weather information ``` +Or, to drive an iOS device automation task (requires PyAutoGUI server setup and device mirroring): + +```yaml +ios: + serverPort: 1412 + mirrorConfig: + mirrorX: 100 + mirrorY: 200 + mirrorWidth: 400 + mirrorHeight: 800 + +tasks: + - name: Search for weather + flow: + - ai: Open Safari browser + - ai: Navigate to bing.com + - ai: Search for "today's weather" + - sleep: 3000 + - aiAssert: The results show weather information +``` + Run the script: ```bash @@ -177,6 +198,37 @@ android: output: ``` +### The `ios` part + +```yaml +ios: + # PyAutoGUI server port, optional, defaults to 1412. + serverPort: + + # PyAutoGUI server URL, optional, defaults to http://localhost:1412. + serverUrl: + + # Whether to automatically dismiss keyboard after input, optional, defaults to false. + autoDismissKeyboard: + + # iOS device mirroring configuration for precise targeting operations + mirrorConfig: + # X position of the mirror on the host display + mirrorX: + # Y position of the mirror on the host display + mirrorY: + # Width of the mirror + mirrorWidth: + # Height of the mirror + mirrorHeight: + + # The launch URL or app, optional, defaults to the device's current page. + launch: + + # The path to the JSON file for outputting aiQuery/aiAssert results, optional. + output: +``` + ### The `tasks` part The `tasks` part is an array that defines the steps of the script. Remember to add a `-` before each step to indicate it's an array item. @@ -304,6 +356,11 @@ The command-line tool provides several options to control the execution behavior - `--web.viewportWidth `: Sets the browser viewport width, which will override the `web.viewportWidth` parameter in all script files. - `--web.viewportHeight `: Sets the browser viewport height, which will override the `web.viewportHeight` parameter in all script files. - `--android.deviceId `: Sets the Android device ID, which will override the `android.deviceId` parameter in all script files. +- `--ios.serverPort `: Sets the iOS PyAutoGUI server port, which will override the `ios.serverPort` parameter in all script files. +- `--ios.mirrorX `: Sets the iOS mirror X position, which will override the `ios.mirrorConfig.mirrorX` parameter in all script files. +- `--ios.mirrorY `: Sets the iOS mirror Y position, which will override the `ios.mirrorConfig.mirrorY` parameter in all script files. +- `--ios.mirrorWidth `: Sets the iOS mirror width, which will override the `ios.mirrorConfig.mirrorWidth` parameter in all script files. +- `--ios.mirrorHeight `: Sets the iOS mirror height, which will override the `ios.mirrorConfig.mirrorHeight` parameter in all script files. - `--dotenv-debug`: Sets the debug log for dotenv, disabled by default. - `--dotenv-override`: Sets whether dotenv overrides global environment variables with the same name, disabled by default. diff --git a/apps/site/docs/zh/automate-with-scripts-in-yaml.mdx b/apps/site/docs/zh/automate-with-scripts-in-yaml.mdx index 13ef5966b..e415c04c9 100644 --- a/apps/site/docs/zh/automate-with-scripts-in-yaml.mdx +++ b/apps/site/docs/zh/automate-with-scripts-in-yaml.mdx @@ -80,6 +80,27 @@ tasks: - aiAssert: 结果显示天气信息 ``` +或者驱动 iOS 设备的自动化任务(需要设置 PyAutoGUI 服务器和设备镜像) + +```yaml +ios: + serverPort: 1412 + mirrorConfig: + mirrorX: 100 + mirrorY: 200 + mirrorWidth: 400 + mirrorHeight: 800 + +tasks: + - name: 搜索天气 + flow: + - ai: 打开 Safari 浏览器 + - ai: 导航到 bing.com + - ai: 搜索 "今日天气" + - sleep: 3000 + - aiAssert: 结果显示天气信息 +``` + 运行脚本 ```bash @@ -177,6 +198,37 @@ android: output: ``` +### `ios` 部分 + +```yaml +ios: + # PyAutoGUI 服务器端口,可选,默认 1412 + serverPort: + + # PyAutoGUI 服务器 URL,可选,默认 http://localhost:1412 + serverUrl: + + # 输入后是否自动隐藏键盘,可选,默认 false + autoDismissKeyboard: + + # iOS 设备镜像配置,用于精确定位操作 + mirrorConfig: + # 镜像在主显示器上的 X 位置 + mirrorX: + # 镜像在主显示器上的 Y 位置 + mirrorY: + # 镜像的宽度 + mirrorWidth: + # 镜像的高度 + mirrorHeight: + + # 启动 URL 或应用,可选,默认使用设备当前页面 + launch: + + # 输出 aiQuery/aiAssert 结果的 JSON 文件路径,可选 + output: +``` + ### `tasks` 部分 `tasks` 部分是一个数组,定义了脚本执行的步骤。记得在每个步骤前添加 `-` 符号,表明这些步骤是个数组。 @@ -308,6 +360,11 @@ midscene './scripts/**/*.yaml' - `--web.viewportWidth `: 设置浏览器视口宽度,这将覆盖所有脚本文件中的 `web.viewportWidth` 参数。 - `--web.viewportHeight `: 设置浏览器视口高度,这将覆盖所有脚本文件中的 `web.viewportHeight` 参数。 - `--android.deviceId `: 设置安卓设备 ID,这将覆盖所有脚本文件中的 `android.deviceId` 参数。 +- `--ios.serverPort `: 设置 iOS PyAutoGUI 服务器端口,这将覆盖所有脚本文件中的 `ios.serverPort` 参数。 +- `--ios.mirrorX `: 设置 iOS 镜像 X 位置,这将覆盖所有脚本文件中的 `ios.mirrorConfig.mirrorX` 参数。 +- `--ios.mirrorY `: 设置 iOS 镜像 Y 位置,这将覆盖所有脚本文件中的 `ios.mirrorConfig.mirrorY` 参数。 +- `--ios.mirrorWidth `: 设置 iOS 镜像宽度,这将覆盖所有脚本文件中的 `ios.mirrorConfig.mirrorWidth` 参数。 +- `--ios.mirrorHeight `: 设置 iOS 镜像高度,这将覆盖所有脚本文件中的 `ios.mirrorConfig.mirrorHeight` 参数。 - `--dotenv-debug`: 设置 dotenv 的 debug 日志,默认关闭。 - `--dotenv-override`: 设置 dotenv 是否覆盖同名的全局环境变量,默认关闭。 diff --git a/packages/cli/package.json b/packages/cli/package.json index db90462f1..8d024d1cc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,6 +21,7 @@ "dependencies": { "@midscene/android": "workspace:*", "@midscene/core": "workspace:*", + "@midscene/ios": "workspace:*", "@midscene/shared": "workspace:*", "@midscene/web": "workspace:*", "http-server": "14.1.1", diff --git a/packages/cli/src/cli-utils.ts b/packages/cli/src/cli-utils.ts index 5a3add3d6..b37bb8508 100644 --- a/packages/cli/src/cli-utils.ts +++ b/packages/cli/src/cli-utils.ts @@ -89,6 +89,31 @@ Usage: type: 'string', description: 'Override device ID for Android environments.', }, + 'ios.server-port': { + alias: 'ios.serverPort', + type: 'number', + description: 'Override PyAutoGUI server port for iOS environments.', + }, + 'ios.mirror-x': { + alias: 'ios.mirrorConfig.mirrorX', + type: 'number', + description: 'Override mirror X position for iOS environments.', + }, + 'ios.mirror-y': { + alias: 'ios.mirrorConfig.mirrorY', + type: 'number', + description: 'Override mirror Y position for iOS environments.', + }, + 'ios.mirror-width': { + alias: 'ios.mirrorConfig.mirrorWidth', + type: 'number', + description: 'Override mirror width for iOS environments.', + }, + 'ios.mirror-height': { + alias: 'ios.mirrorConfig.mirrorHeight', + type: 'number', + description: 'Override mirror height for iOS environments.', + }, }) .version('version', 'Show version number', __VERSION__) .help() diff --git a/packages/cli/src/config-factory.ts b/packages/cli/src/config-factory.ts index d5eb14d85..9364942ea 100644 --- a/packages/cli/src/config-factory.ts +++ b/packages/cli/src/config-factory.ts @@ -4,6 +4,7 @@ import { cwd } from 'node:process'; import type { MidsceneYamlConfig, MidsceneYamlScriptAndroidEnv, + MidsceneYamlScriptIOSEnv, MidsceneYamlScriptWebEnv, } from '@midscene/core'; import { interpolateEnvVars } from '@midscene/web/yaml'; @@ -33,6 +34,7 @@ export interface ConfigFactoryOptions { dotenvDebug?: boolean; web?: Partial; android?: Partial; + ios?: Partial; } export interface ParsedConfig { @@ -42,6 +44,7 @@ export interface ParsedConfig { shareBrowserContext: boolean; web?: MidsceneYamlScriptWebEnv; android?: MidsceneYamlScriptAndroidEnv; + ios?: MidsceneYamlScriptIOSEnv; target?: MidsceneYamlScriptWebEnv; files: string[]; patterns: string[]; // Keep patterns for reference @@ -118,6 +121,7 @@ export async function parseConfigYaml( configYaml.shareBrowserContext ?? defaultConfig.shareBrowserContext, web: configYaml.web, android: configYaml.android, + ios: configYaml.ios, patterns: configYaml.files, files, headed: configYaml.headed ?? defaultConfig.headed, @@ -138,11 +142,13 @@ export async function createConfig( { web: parsedConfig.web, android: parsedConfig.android, + ios: parsedConfig.ios, target: parsedConfig.target, }, { web: options?.web, android: options?.android, + ios: options?.ios, }, ); diff --git a/packages/cli/src/create-yaml-player.ts b/packages/cli/src/create-yaml-player.ts index 5f2c14ae1..b5a8b189b 100644 --- a/packages/cli/src/create-yaml-player.ts +++ b/packages/cli/src/create-yaml-player.ts @@ -10,6 +10,7 @@ import type { MidsceneYamlScript, MidsceneYamlScriptEnv, } from '@midscene/core'; +import { agentFromPyAutoGUI } from '@midscene/ios'; import { AgentOverChromeBridge } from '@midscene/web/bridge-mode'; import { puppeteerAgentForTarget } from '@midscene/web/puppeteer-agent-launcher'; import type { Browser } from 'puppeteer'; @@ -161,8 +162,26 @@ export async function createYamlPlayer( return { agent, freeFn }; } + // handle ios + if (typeof yamlScript.ios !== 'undefined') { + const iosTarget = yamlScript.ios; + const agent = await agentFromPyAutoGUI({ + serverUrl: iosTarget.serverUrl, + serverPort: iosTarget.serverPort, + autoDismissKeyboard: iosTarget.autoDismissKeyboard, + mirrorConfig: iosTarget.mirrorConfig, + }); + + freeFn.push({ + name: 'destroy_ios_agent', + fn: () => agent.destroy(), + }); + + return { agent, freeFn }; + } + throw new Error( - 'No valid target configuration found in the yaml script, should be either "web" or "android"', + 'No valid target configuration found in the yaml script, should be either "web", "android", or "ios"', ); }, undefined, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4a9c2eee6..3115e7a90 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -8,6 +8,17 @@ import { createConfig, createFilesConfig } from './config-factory'; Promise.resolve( (async () => { + // Load .env file early, before parsing any YAML files that might use env vars + const dotEnvConfigFile = join(process.cwd(), '.env'); + if (existsSync(dotEnvConfigFile)) { + console.log(` Env file: ${dotEnvConfigFile}`); + dotenv.config({ + path: dotEnvConfigFile, + debug: false, // Will be set properly later + override: false, // Will be set properly later + }); + } + const { options, path, files: cmdFiles } = await parseProcessArgs(); const welcome = `\nWelcome to @midscene/cli v${version}\n`; @@ -39,6 +50,7 @@ Promise.resolve( dotenvDebug: options['dotenv-debug'], web: options.web, android: options.android, + ios: options.ios, }; let config; @@ -64,9 +76,8 @@ Promise.resolve( process.exit(1); } - const dotEnvConfigFile = join(process.cwd(), '.env'); + // Update dotenv configuration with user-specified options if (existsSync(dotEnvConfigFile)) { - console.log(` Env file: ${dotEnvConfigFile}`); dotenv.config({ path: dotEnvConfigFile, debug: config.dotenvDebug, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b87bfa6d6..7bc731c9b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -21,6 +21,10 @@ export type { MidsceneYamlFlowItem, MidsceneYamlFlowItemAIRightClick, MidsceneYamlConfigResult, + MidsceneYamlScriptWebEnv, + MidsceneYamlScriptAndroidEnv, + MidsceneYamlScriptIOSEnv, + MidsceneYamlConfig, LocateOption, DetailedLocateParam, } from './yaml'; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b5af29eaa..042523bc2 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -547,7 +547,8 @@ export type PageType = | 'playwright' | 'static' | 'chrome-extension-proxy' - | 'android'; + | 'android' + | 'ios'; export interface StreamingCodeGenerationOptions { /** Whether to enable streaming output */ diff --git a/packages/core/src/yaml.ts b/packages/core/src/yaml.ts index 0dae79353..559b84c01 100644 --- a/packages/core/src/yaml.ts +++ b/packages/core/src/yaml.ts @@ -35,6 +35,7 @@ export interface MidsceneYamlScript { target?: MidsceneYamlScriptWebEnv; web?: MidsceneYamlScriptWebEnv; android?: MidsceneYamlScriptAndroidEnv; + ios?: MidsceneYamlScriptIOSEnv; tasks: MidsceneYamlTask[]; } @@ -82,9 +83,28 @@ export interface MidsceneYamlScriptAndroidEnv launch?: string; } +export interface MidsceneYamlScriptIOSEnv extends MidsceneYamlScriptEnvBase { + // The URL or app to launch, optional, will use the current screen if not specified + launch?: string; + + // PyAutoGUI server configuration + serverUrl?: string; + serverPort?: number; + autoDismissKeyboard?: boolean; + + // iOS device mirroring configuration to define the mirror position and size + mirrorConfig?: { + mirrorX: number; + mirrorY: number; + mirrorWidth: number; + mirrorHeight: number; + }; +} + export type MidsceneYamlScriptEnv = | MidsceneYamlScriptWebEnv - | MidsceneYamlScriptAndroidEnv; + | MidsceneYamlScriptAndroidEnv + | MidsceneYamlScriptIOSEnv; export interface MidsceneYamlFlowItemAIAction { ai?: string; // this is the shortcut for aiAction @@ -212,6 +232,7 @@ export interface MidsceneYamlConfig { shareBrowserContext?: boolean; web?: MidsceneYamlScriptWebEnv; android?: MidsceneYamlScriptAndroidEnv; + ios?: MidsceneYamlScriptIOSEnv; files: string[]; headed?: boolean; keepWindow?: boolean; diff --git a/packages/ios-playground/README.md b/packages/ios-playground/README.md new file mode 100644 index 000000000..f3b91119f --- /dev/null +++ b/packages/ios-playground/README.md @@ -0,0 +1,147 @@ +# @midscene/ios-playground + +iOS playground for Midscene.js - Control iOS devices through natural language commands using screen mirroring. + +## Quick Start + +```bash +npx @midscene/ios-playground +``` + +This will: + +1. Automatically start the PyAutoGUI server on port 1412 +2. Launch the iOS playground web interface +3. Open the playground in your default browser + +## Prerequisites + +1. **macOS System**: iOS playground requires macOS (tested on macOS 11 and later). + +2. **Python 3 and Dependencies**: The playground will automatically manage the PyAutoGUI server, but you need Python 3 with required packages: + + ```bash + pip3 install pyautogui flask flask-cors + ``` + +3. **iPhone Mirroring**: Use iPhone Mirroring (macOS Sequoia) to mirror your physical iPhone to your Mac screen. + +4. **AI Model Configuration**: Set up your AI model credentials. See [Midscene documentation](https://midscenejs.com/choose-a-model) for supported models. + +## Features + +- **Automatic Server Management**: PyAutoGUI server starts and stops automatically +- **Auto-Detection**: Automatically detects iPhone Mirroring window position and size +- **Natural Language Control**: Control iOS devices using natural language commands +- **Screenshot Capture**: Takes screenshots of only the iOS mirrored region +- **Coordinate Transformation**: Automatically maps iOS coordinates to macOS screen coordinates +- **Real-time Interaction**: Direct interaction with iOS interface elements through AI + +## Usage + +1. **Start the playground**: + + ```bash + npx @midscene/ios-playground + ``` + +2. **Set up iPhone Mirroring**: Open iPhone Mirroring app on your Mac (macOS Sequoia) and connect your iPhone + +3. **Configure AI Model**: In the playground web interface, configure your AI model credentials + +4. **Auto-detect or Manual Setup**: + - Click "Auto Detect iOS Mirror" for automatic configuration, or + - Manually set the mirror region coordinates + +5. **Use natural language commands** to interact with your iOS device: + + - **Action**: "tap the Settings app" + - **Query**: "extract the battery percentage" + - **Assert**: "the home screen is visible" + +## Development + +To run the playground in development mode: + +```bash +cd packages/ios-playground +npm install +npm run dev:server +``` + +This will build the project and start the server locally. + +## Architecture + +The iOS playground architecture consists of: + +- **Frontend**: Web-based interface for AI interaction (built with React/TypeScript) +- **Playground Server**: Express.js server that bridges between frontend and iOS automation +- **PyAutoGUI Server**: Python Flask server for screen capture and input control +- **iPhone Mirroring**: macOS iPhone Mirroring for device display +- **Midscene AI Core**: AI-powered automation engine with iOS device adapter +- **Coordinate Transformation**: Automatic mapping between iOS logical coordinates and macOS screen coordinates + +## How It Works + +1. **Screen Mirroring**: iOS device screen is displayed on macOS through iPhone Mirroring +2. **Auto-Detection**: Python server detects the mirroring window position and size using AppleScript +3. **Coordinate Mapping**: iOS logical coordinates (e.g., 200, 400) are automatically transformed to macOS screen coordinates +4. **AI Processing**: Midscene AI analyzes screenshots and determines actions based on natural language commands +5. **Action Execution**: Actions are executed on the macOS screen within the iOS mirrored region + +## Troubleshooting + +### PyAutoGUI Server Issues + +If the PyAutoGUI server fails to start automatically, check: + +```bash +# Check if port 1412 is available +lsof -i :1412 +# Manually start the server +cd packages/ios +node bin/server.js 1412 +``` + +### iPhone Mirroring Detection Issues + +1. Ensure iPhone Mirroring app is open and visible on screen +2. Try clicking "Auto Detect iOS Mirror" in the playground interface +3. Manually configure mirror coordinates if auto-detection fails +4. Check that the iPhone Mirroring window is not minimized + +### Permission Issues + +On macOS, you may need to grant the following permissions: + +- **Accessibility**: System Preferences > Security & Privacy > Privacy > Accessibility +- **Screen Recording**: System Preferences > Security & Privacy > Privacy > Screen Recording + +Add Terminal, Python, or your development environment to these permission lists. + +### Python Dependencies + +If you encounter Python-related errors: + +```bash +# Install or upgrade required packages +pip3 install --upgrade pyautogui flask flask-cors + +# On macOS, you might need to install using conda or homebrew +brew install python@3.11 +``` + +### Mirror Region Configuration + +If clicks are not landing in the right place: + +1. Use the "Auto Detect iOS Mirror" feature first +2. If manual configuration is needed, measure the exact position and size of your iPhone Mirroring window +3. Account for window borders and title bars when setting coordinates + +## Related Documentation + +- [Midscene.js Documentation](https://midscenejs.com/) +- [API Reference](https://midscenejs.com/api) +- [Choosing AI Models](https://midscenejs.com/choose-a-model) diff --git a/packages/ios-playground/bin/ios-playground b/packages/ios-playground/bin/ios-playground new file mode 100755 index 000000000..9341e503e --- /dev/null +++ b/packages/ios-playground/bin/ios-playground @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +const path = require('path'); +const { spawn } = require('child_process'); + +// Get the directory where this script is located +const binDir = __dirname; +// The server script should be in the same directory as this bin script +const serverScript = path.join(binDir, 'server.js'); + +console.log('Starting iOS Playground server...'); + +// Start the server +const serverProcess = spawn('node', [serverScript], { + stdio: 'inherit', + env: { ...process.env, NODE_ENV: 'production' } +}); + +// Handle process termination +process.on('SIGINT', () => { + console.log('\nShutting down iOS Playground server...'); + serverProcess.kill('SIGINT'); + process.exit(0); +}); + +process.on('SIGTERM', () => { + serverProcess.kill('SIGTERM'); + process.exit(0); +}); + +serverProcess.on('close', (code) => { + console.log(`iOS Playground server exited with code ${code}`); + process.exit(code); +}); diff --git a/packages/ios-playground/bin/server.js b/packages/ios-playground/bin/server.js new file mode 100644 index 000000000..ed405d1d9 --- /dev/null +++ b/packages/ios-playground/bin/server.js @@ -0,0 +1,162 @@ +const path = require('path'); +const { spawn } = require('child_process'); +const { iOSDevice, iOSAgent } = require('@midscene/ios'); +const { PLAYGROUND_SERVER_PORT } = require('@midscene/shared/constants'); +const PlaygroundServer = require('@midscene/web/midscene-server').default; + +const staticDir = path.join(__dirname, '..', '..', '..', 'apps', 'ios-playground', 'dist'); +const playgroundServer = new PlaygroundServer( + iOSDevice, + iOSAgent, + staticDir, +); + +// Auto server management +let autoServerProcess = null; +const AUTO_SERVER_PORT = 1412; + +/** + * Check if auto server is running on the specified port + */ +const checkAutoServerRunning = async (port = AUTO_SERVER_PORT) => { + return new Promise((resolve) => { + const net = require('net'); + const client = new net.Socket(); + + client.setTimeout(1000); + + client.on('connect', () => { + client.destroy(); + resolve(true); + }); + + client.on('timeout', () => { + client.destroy(); + resolve(false); + }); + + client.on('error', () => { + resolve(false); + }); + + client.connect(port, 'localhost'); + }); +}; + +/** + * Start the auto server if it's not running + */ +const startAutoServer = async () => { + try { + const isRunning = await checkAutoServerRunning(); + + if (isRunning) { + console.log(`✅ PyAutoGUI server is already running on port ${AUTO_SERVER_PORT}`); + return true; + } + + console.log(`🚀 Starting PyAutoGUI server on port ${AUTO_SERVER_PORT}...`); + + // Find the auto server script path + const autoServerPath = path.join(__dirname, '..', '..', 'ios', 'bin', 'server.js'); + + // Start the auto server process + autoServerProcess = spawn('node', [autoServerPath, AUTO_SERVER_PORT], { + stdio: 'pipe', + env: { + ...process.env, + NODE_ENV: 'production' + } + }); + + // Handle auto server output + autoServerProcess.stdout.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + console.log(`[PyAutoGUI] ${output}`); + } + }); + + autoServerProcess.stderr.on('data', (data) => { + const output = data.toString().trim(); + if (output) { + console.error(`[PyAutoGUI Error] ${output}`); + } + }); + + autoServerProcess.on('error', (error) => { + console.error('Failed to start PyAutoGUI server:', error); + }); + + autoServerProcess.on('close', (code) => { + if (code !== 0) { + console.error(`PyAutoGUI server exited with code ${code}`); + } + autoServerProcess = null; + }); + + // Wait a bit for the server to start + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Verify it's running + const isNowRunning = await checkAutoServerRunning(); + if (isNowRunning) { + console.log(`✅ PyAutoGUI server started successfully on port ${AUTO_SERVER_PORT}`); + return true; + } else { + console.error(`❌ Failed to start PyAutoGUI server on port ${AUTO_SERVER_PORT}`); + return false; + } + + } catch (error) { + console.error('Error starting auto server:', error); + return false; + } +}; + +const main = async () => { + try { + // Start auto server first + await startAutoServer(); + + await playgroundServer.launch(PLAYGROUND_SERVER_PORT); + console.log( + `Midscene iOS Playground server is running on http://localhost:${playgroundServer.port}`, + ); + + // Automatically open browser + if (process.env.NODE_ENV !== 'test') { + try { + const { default: open } = await import('open'); + await open(`http://localhost:${playgroundServer.port}`); + } catch (error) { + console.log('Could not open browser automatically. Please visit the URL manually.'); + } + } + } catch (error) { + console.error('Failed to start iOS playground server:', error); + process.exit(1); + } +}; + +// Handle graceful shutdown +const cleanup = () => { + console.log('Shutting down gracefully...'); + + if (playgroundServer) { + playgroundServer.close(); + } + + if (autoServerProcess) { + console.log('Stopping PyAutoGUI server...'); + autoServerProcess.kill('SIGTERM'); + autoServerProcess = null; + } + + process.exit(0); +}; + +process.on('SIGTERM', cleanup); +process.on('SIGINT', cleanup); + +main(); diff --git a/packages/ios-playground/modern.config.ts b/packages/ios-playground/modern.config.ts new file mode 100644 index 000000000..ff87335a3 --- /dev/null +++ b/packages/ios-playground/modern.config.ts @@ -0,0 +1,13 @@ +import { moduleTools, defineConfig } from '@modern-js/module-tools'; + +export default defineConfig({ + plugins: [moduleTools()], + buildConfig: { + buildType: 'bundle', + format: 'cjs', + target: 'es2019', + outDir: './dist', + dts: false, + externals: ['express', 'cors', 'open'], + }, +}); diff --git a/packages/ios-playground/package.json b/packages/ios-playground/package.json new file mode 100644 index 000000000..622eaab01 --- /dev/null +++ b/packages/ios-playground/package.json @@ -0,0 +1,34 @@ +{ + "name": "@midscene/ios-playground", + "version": "0.25.3", + "description": "iOS playground for Midscene", + "main": "./dist/lib/index.js", + "types": "./dist/types/index.d.ts", + "files": ["dist", "static", "bin", "README.md"], + "bin": { + "midscene-ios-playground": "./bin/ios-playground", + "@midscene/ios-playground": "./bin/ios-playground" + }, + "scripts": { + "dev": "modern dev", + "dev:server": "npm run build && ./bin/ios-playground", + "build": "modern build -c ./modern.config.ts", + "build:watch": "modern build -w -c ./modern.config.ts --no-clear" + }, + "dependencies": { + "@midscene/ios": "workspace:*", + "@midscene/shared": "workspace:*", + "@midscene/web": "workspace:*", + "cors": "2.8.5", + "express": "^4.21.2", + "open": "10.1.0" + }, + "devDependencies": { + "@modern-js/module-tools": "2.60.6", + "@types/cors": "2.8.12", + "@types/express": "^4.17.21", + "@types/node": "^18.0.0", + "typescript": "^5.8.3" + }, + "license": "MIT" +} diff --git a/packages/ios-playground/test-health.js b/packages/ios-playground/test-health.js new file mode 100644 index 000000000..88618bbc8 --- /dev/null +++ b/packages/ios-playground/test-health.js @@ -0,0 +1,28 @@ +const fetch = require('node-fetch'); + +async function testHealth() { + try { + console.log('Testing health check...'); + const response = await fetch('http://localhost:5800/status'); + const data = await response.json(); + console.log('Status:', response.status); + console.log('Data:', data); + console.log('Success!'); + } catch (error) { + console.error('Error:', error.message); + } +} + +// Test multiple times like the frontend does +let counter = 0; +const interval = setInterval(async () => { + counter++; + console.log(`\n--- Test ${counter} ---`); + await testHealth(); + + if (counter >= 5) { + clearInterval(interval); + console.log('\nTest completed'); + process.exit(0); + } +}, 2000); diff --git a/packages/ios-playground/tsconfig.json b/packages/ios-playground/tsconfig.json new file mode 100644 index 000000000..3534ba4c7 --- /dev/null +++ b/packages/ios-playground/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["bin/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ios/README.md b/packages/ios/README.md new file mode 100644 index 000000000..836b8b73f --- /dev/null +++ b/packages/ios/README.md @@ -0,0 +1,307 @@ +# @midscene/ios + +iOS automation package for Midscene.js with coordinate mapping support for iOS device mirroring. + +## Features + +- **iOS Device Mirroring**: Control iOS devices through screen mirroring on macOS +- **Coordinate Mapping**: Automatic transformation from iOS coordinates to macOS screen coordinates +- **AI Integration**: Use natural language to interact with iOS interfaces +- **Screenshot Capture**: Take region-specific screenshots of iOS mirrors +- **PyAutoGUI Backend**: Reliable macOS system control through Python server + +## Installation + +```bash +npm install @midscene/ios +``` + +## Prerequisites + +1. **Python 3** with required packages: + + ```bash + pip3 install flask pyautogui + ``` + +2. **macOS Accessibility Permissions**: + - Go to System Preferences → Security & Privacy → Privacy → Accessibility + - Add your terminal application to the list + - Required for PyAutoGUI to control mouse and keyboard + +3. **iOS Device Mirroring**: + - iPhone Mirroring (macOS Sequoia) + +## Getting iPhone Mirroring Window Coordinates + +To use iOS automation, you need to determine where the iPhone Mirroring window is positioned on your macOS screen. We provide a helpful AppleScript that automatically detects this for you. + +### Using the AppleScript + +```bash +# Navigate to the iOS package directory +cd packages/ios + +# Run the script to get window coordinates +osascript scripts/getAppWindowRect.scpt +``` + +**Important**: The script gives you 4 seconds to make the iPhone Mirroring app the foreground window before it captures the coordinates. + +The output will look like: + +```text +{"iPhone Mirroring", {692, 161}, {344, 764}} +``` + +This means: + +- App name: "iPhone Mirroring" +- Position: x=692, y=161 (use these for `mirrorX` and `mirrorY`) +- Size: width=344, height=764 (use these for `mirrorWidth` and `mirrorHeight`) + +## Quick Start + +### 1. Start PyAutoGUI Server + +```bash +cd packages/ios/idb +python3 auto_server.py 1412 +``` + +### 2. Configure iOS Mirroring + +First, get the mirror window coordinates using the AppleScript mentioned above, then: + +```typescript +import { iOSDevice, iOSAgent } from '@midscene/ios'; + +const device = new iOSDevice({ + serverPort: 1412, + mirrorConfig: { + mirrorX: 692, // Mirror position on macOS screen + mirrorY: 161, + mirrorWidth: 344, // Mirror size on macOS screen + mirrorHeight: 764 + } +}); + +await device.connect(); +const agent = new iOSAgent(device); + +// AI interactions with automatic coordinate mapping +await agent.aiTap('Settings app'); +await agent.aiInput('Wi-Fi', 'Search settings'); +const settings = await agent.aiQuery('string[], visible settings'); +``` + +### 3. Basic Device Control + +```typescript +// Direct coordinate operations +await device.tap({ left: 100, top: 200 }); +await device.input('Hello', { left: 150, top: 300 }); +await device.scroll({ direction: 'down', distance: 200 }); + +// Screenshots (automatically crops to iOS mirror region) +const screenshot = await device.screenshotBase64(); +``` + +## API Reference + +### agentFromPyAutoGUI(options?) + +Creates an iOS agent with PyAutoGUI backend. + +**Options:** + +- `serverUrl?: string` - Custom server URL (default: `http://localhost:1412`) +- `serverPort?: number` - Server port (default: `1412`) +- `autoDismissKeyboard?: boolean` - Auto dismiss keyboard (not applicable for desktop) + +### iOSDevice Methods + +#### `launch(uri: string): Promise` + +Launch an application or URL. + +- For URLs: `await device.launch('https://example.com')` +- For apps: `await device.launch('Safari')` + +#### `size(): Promise` + +Get screen dimensions and pixel ratio. + +#### `screenshotBase64(): Promise` + +Take a screenshot and return as base64 string. + +#### `tap(point: Point): Promise` + +Click at the specified coordinates. + +#### `hover(point: Point): Promise` + +Move mouse to the specified coordinates. + +#### `input(text: string): Promise` + +Type text using the keyboard. + +#### `keyboardPress(key: string): Promise` + +Press a specific key. Supported keys: + +- `'Return'`, `'Enter'` - Enter key +- `'Tab'` - Tab key +- `'Space'` - Space bar +- `'Backspace'` - Backspace +- `'Delete'` - Delete key +- `'Escape'` - Escape key + +#### `scroll(options: ScrollOptions): Promise` + +Scroll in the specified direction. + +**ScrollOptions:** + +- `direction: 'up' | 'down' | 'left' | 'right'` +- `distance?: number` - Scroll distance in pixels (default: 100) + +## PyAutoGUI Server API + +The Python server accepts POST requests to `/run` with JSON payloads: + +### Supported Actions + +#### Click + +```json +{ + "action": "click", + "x": 100, + "y": 100 +} +``` + +#### Move (Hover) + +```json +{ + "action": "move", + "x": 200, + "y": 200, + "duration": 0.2 +} +``` + +#### Drag + +```json +{ + "action": "drag", + "x": 100, + "y": 100, + "x2": 200, + "y2": 200, + "duration": 0.5 +} +``` + +#### Type + +```json +{ + "action": "type", + "text": "Hello World", + "interval": 0.0 +} +``` + +#### Key Press + +```json +{ + "action": "key", + "key": "return" +} +``` + +#### Hotkey Combination + +```json +{ + "action": "hotkey", + "keys": ["cmd", "c"] +} +``` + +#### Scroll + +```json +{ + "action": "scroll", + "x": 400, + "y": 300, + "clicks": 3 +} +``` + +#### Sleep + +```json +{ + "action": "sleep", + "seconds": 1.0 +} +``` + +### Health Check + +GET `/health` - Returns server status and screen information. + +## Architecture + +```text +┌─────────────────┐ HTTP ┌─────────────────┐ PyAutoGUI ┌─────────────────┐ +│ TypeScript │ ────> │ Python Server │ ─────────> │ macOS System │ +│ iOS Agent │ │ (Flask + PyAutoGUI) │ │ (Mouse/Keyboard) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## Troubleshooting + +### Accessibility Permissions + +If you get permission errors, ensure your terminal has accessibility permissions: + +1. System Preferences → Security & Privacy → Privacy +2. Select "Accessibility" from the left sidebar +3. Click the lock to make changes +4. Add your terminal application to the list + +### Python Dependencies + +```bash +# Install required Python packages +pip3 install flask pyautogui + +# On macOS, you might also need: +pip3 install pillow +``` + +### Port Already in Use + +If port 1412 is already in use, specify a different port: + +```typescript +const agent = await agentFromPyAutoGUI({ serverPort: 1413 }); +``` + +## Example + +See `examples/ios-mirroring-demo.js` for a complete usage example. + +## License + +MIT diff --git a/packages/ios/bin/server.js b/packages/ios/bin/server.js new file mode 100755 index 000000000..e84645c02 --- /dev/null +++ b/packages/ios/bin/server.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +import { spawn, execSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const serverPath = join(__dirname, '../idb/auto_server.py'); + +const port = process.argv[2] || '1412'; + +console.log(`Starting PyAutoGUI server on port ${port}...`); + +// kill process on port 1412 first +try { + execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: 'ignore' }); + console.log(`Killed existing process on port ${port}`); +} catch (error) { + console.error(`Failed to kill process on port ${port}:`, error); +} + +const server = spawn('python3', [serverPath, port], { + stdio: 'inherit', + env: { + ...process.env, + PYTHONUNBUFFERED: '1', + }, +}); + +server.on('error', (error) => { + console.error('Failed to start PyAutoGUI server:', error); + process.exit(1); +}); + +server.on('close', (code) => { + console.log(`PyAutoGUI server exited with code ${code}`); + process.exit(code || 0); +}); + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nShutting down PyAutoGUI server...'); + server.kill('SIGINT'); +}); + +process.on('SIGTERM', () => { + console.log('\nShutting down PyAutoGUI server...'); + server.kill('SIGTERM'); +}); \ No newline at end of file diff --git a/packages/ios/examples/ios-input-example.yaml b/packages/ios/examples/ios-input-example.yaml new file mode 100644 index 000000000..fce09e192 --- /dev/null +++ b/packages/ios/examples/ios-input-example.yaml @@ -0,0 +1,33 @@ +# iOS automation with YAML script example +# This example shows how to automate iOS devices using PyAutoGUI server + +ios: + # PyAutoGUI server configuration + serverUrl: "http://localhost:1412" + + # Auto dismiss keyboard after input (optional) + autoDismissKeyboard: false + + # iOS device mirroring configuration for precise location targeting + # These values define the position and size of the mirrored device screen + + # Output file for aiQuery/aiAssert results (optional) + output: "./results.json" + +tasks: + - name: Open music app and search Coldplay + flow: + - sleep: 5000 + - aiAction: "打开音乐应用" + - sleep: 2000 + - aiTap: "搜索图标" + - sleep: 3000 + - aiInput: "Coldplay" + locate: "底部Search input field" + - sleep: 2000 + - aiKeyboardPress: "Enter" + - sleep: 3000 + - aiWaitFor: "Search results are displayed" + - aiAction: "播放第一首歌曲" + - sleep: 3000 + - aiAction: "返回Home" diff --git a/packages/ios/examples/ios-mirroring-demo.js b/packages/ios/examples/ios-mirroring-demo.js new file mode 100644 index 000000000..1e8af4ef1 --- /dev/null +++ b/packages/ios/examples/ios-mirroring-demo.js @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +/** + * Complete example showing how to use iOS device mirroring with coordinate mapping and enhanced scrolling + * + * This example demonstrates: + * 1. Setting up iOS device mirroring configuration + * 2. Using coordinate transformation for accurate touch events + * 3. Enhanced scrolling with mouse wheel/trackpad for iOS mirror compatibility + * 4. Taking region-specific screenshots + * 5. Automating iOS apps through macOS screen mirroring + * + * Key improvements: + * - Uses mouse wheel/trackpad scrolling instead of drag for better iOS mirror compatibility + * - Proper coordinate handling that prevents focus loss + * - Unified scrolling method that works for both iOS mirroring and regular modes + */ + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +// Load environment variables from .env file +import dotenv from 'dotenv'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Load .env file from the package root directory +dotenv.config({ path: path.join(__dirname, '..', '.env') }); + +// Use dynamic import for TypeScript modules +let iOSDevice; +let iOSAgent; + +async function loadModules() { + try { + const pageModule = await import('../src/page/index.ts'); + const agentModule = await import('../src/agent/index.ts'); + iOSDevice = pageModule.iOSDevice; + iOSAgent = agentModule.iOSAgent; + } catch (error) { + console.error('❌ Failed to load modules. Please build the project first:'); + console.error(' npm run build'); + console.error(' Or run with tsx: npx tsx examples/ios-mirroring-demo.js'); + process.exit(1); + } +} + +async function demonstrateIOSMirroring() { + console.log('🍎 iOS Device Mirroring Demo with Midscene.js'); + console.log('===============================================\n'); + + // Load modules first + await loadModules(); + + // Step 1: Configure iOS device mirroring + console.log('📱 Step 1: Setting up iOS device mirroring...'); + + const device = new iOSDevice({ + serverPort: 1412 + }); + + try { + console.log('🔗 Connecting to iOS device...'); + await device.connect(); + + // Verify configuration + const config = await device.getConfiguration(); + console.log('✅ iOS mirroring configured successfully!'); + console.log( + ` 🖥️ Mirror Region: (${config.config.mirror_x}, ${config.config.mirror_y}) ${config.config.mirror_width}x${config.config.mirror_height}`, + ); + + // Step 2: Initialize AI agent + console.log('🤖 Step 2: Initializing AI agent...'); + const agent = new iOSAgent(device); + console.log('✅ AI agent ready!\n'); + + // Step 3: Take iOS region screenshot + console.log('📸 Step 3: Taking iOS region screenshot...'); + const screenshot = await device.screenshotBase64(); + console.log(`✅ Screenshot captured (${screenshot.length} bytes)`); + console.log(' 💾 Screenshot contains only the iOS mirrored area\n'); + + // Step 4: Test scrolling + console.log( + ' 🔄 Testing horizontal scroll right (300px) - should scroll horizontally to the right:', + ); + await device.scroll({ direction: 'right', distance: 300 }); + await new Promise((resolve) => setTimeout(resolve, 1500)); + + console.log( + ' ⬅️ Testing horizontal scroll left (300px) - should scroll horizontally to the left:', + ); + await device.scroll({ direction: 'left', distance: 300 }); + await new Promise((resolve) => setTimeout(resolve, 1500)); + + console.log('✅ Enhanced horizontal scrolling test completed!\n'); + + // Step 5: Demonstrate AI automation + console.log('🧠 Step 5: AI automation example...'); + console.log(' (This would work with actual iOS app content)'); + + // Example AI operations (commented out as they need actual iOS app content) + await agent.aiTap('Settings app icon'); + await agent.ai('Go back to home screen'); + // await agent.ai("Enable dark mode in Display & Brightness settings") + + console.log('✅ Demo completed successfully!\n'); + + // Step 6: Show usage summary + console.log('📋 Step 6: Usage Summary:'); + console.log('================'); + console.log( + '• iOS coordinates are automatically transformed to macOS coordinates', + ); + console.log('• Screenshots capture only the iOS mirrored region'); + console.log( + '• Scrolling now uses intelligent distance mapping for better Android compatibility', + ); + console.log( + '• Distance values (e.g., 200px) are automatically converted to appropriate scroll events', + ); + console.log( + '• Trackpad scrolling by default provides smooth, natural iOS experience', + ); + console.log('• Mouse wheel scrolling available as fallback option'); + console.log('• All Midscene AI features work with iOS device mirroring'); + console.log('• Perfect for testing iOS apps through screen mirroring'); + console.log( + '• Coordinate system is unified: use iOS logical coordinates everywhere\n', + ); + } catch (error) { + console.error('❌ Demo failed:', error.message); + console.error('\n🔧 Troubleshooting:'); + console.error( + '• Ensure Python server is running: python3 auto_server.py 1412', + ); + console.error('• Check iOS device is properly mirrored on macOS screen'); + console.error('• Verify mirror coordinates match actual screen position'); + console.error('• Install required dependencies: flask, pyautogui'); + process.exit(1); + } +} + +// Additional utility functions for iOS mirroring setup +async function detectIOSMirrorRegion() { + console.log('🔍 iOS Mirror Region Detection Helper'); + console.log('===================================='); + console.log( + 'Use this function to help detect iOS mirror coordinates on your screen:', + ); + console.log('1. Open iOS device in screen mirroring/QuickTime/Simulator'); + console.log('2. Note the position and size of the iOS window'); + console.log('3. Update mirrorConfig with these values'); + console.log('4. Test with small tap operations first'); + + // This could be enhanced with screen capture analysis + // to automatically detect iOS mirror regions +} + +function calculateMirrorInfo(mirrorWidth, mirrorHeight) { + // Common iOS device aspect ratios for reference + const commonRatios = [ + { name: 'iPhone 15 Pro', width: 393, height: 852 }, + { name: 'iPhone 12/13/14', width: 390, height: 844 }, + { name: 'iPhone 11 Pro Max', width: 414, height: 896 }, + { name: 'iPhone X/XS', width: 375, height: 812 }, + ]; + + const mirrorRatio = mirrorHeight / mirrorWidth; + + console.log('📐 Mirror Information:'); + console.log(` Mirror Size: ${mirrorWidth}x${mirrorHeight}`); + console.log(` Aspect Ratio: ${mirrorRatio.toFixed(3)}`); + + // Find closest matching iOS device + const closest = commonRatios.reduce((prev, curr) => { + const prevRatio = prev.height / prev.width; + const currRatio = curr.height / curr.width; + return Math.abs(currRatio - mirrorRatio) < Math.abs(prevRatio - mirrorRatio) + ? curr + : prev; + }); + + console.log( + ` Closest iOS device: ${closest.name} (${closest.width}x${closest.height})`, + ); + + return { mirrorWidth, mirrorHeight, suggestedDevice: closest }; +} + +// Check if this file is being run directly +const isMainModule = import.meta.url === `file://${process.argv[1]}`; + +if (isMainModule) { + demonstrateIOSMirroring().catch(console.error); +} diff --git a/packages/ios/idb/auto_server.py b/packages/ios/idb/auto_server.py new file mode 100644 index 000000000..89f1578e8 --- /dev/null +++ b/packages/ios/idb/auto_server.py @@ -0,0 +1,612 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +import pyautogui +import time +import traceback +import sys +import subprocess +import base64 +import io + +app = Flask(__name__) +CORS(app) # Enable CORS for all routes + +# Configure pyautogui +pyautogui.FAILSAFE = True +pyautogui.PAUSE = 0.1 + +def detect_and_configure_ios_mirror(): + """Automatically detect iPhone Mirroring app window and configure mapping""" + try: + # AppleScript to get window information for iPhone Mirroring app + applescript = ''' + tell application "System Events" + try + set mirrorApp to first application process whose name contains "iPhone Mirroring" + set mirrorWindow to first window of mirrorApp + set windowPosition to position of mirrorWindow + set windowSize to size of mirrorWindow + + -- Get window frame information + set windowX to item 1 of windowPosition + set windowY to item 2 of windowPosition + set windowWidth to item 1 of windowSize + set windowHeight to item 2 of windowSize + + -- Try to get the actual visible frame (content area) + try + set appName to name of mirrorApp + set bundleId to bundle identifier of mirrorApp + set visibleFrame to "{" & quote & "found" & quote & ":true," & quote & "x" & quote & ":" & windowX & "," & quote & "y" & quote & ":" & windowY & "," & quote & "width" & quote & ":" & windowWidth & "," & quote & "height" & quote & ":" & windowHeight & "," & quote & "app" & quote & ":" & quote & appName & quote & "," & quote & "bundle" & quote & ":" & quote & bundleId & quote & "}" + return visibleFrame + on error + return "{" & quote & "found" & quote & ":true," & quote & "x" & quote & ":" & windowX & "," & quote & "y" & quote & ":" & windowY & "," & quote & "width" & quote & ":" & windowWidth & "," & quote & "height" & quote & ":" & windowHeight & "}" + end try + + on error errMsg + return "{" & quote & "found" & quote & ":false," & quote & "error" & quote & ":" & quote & errMsg & quote & "}" + end try + end tell + ''' + + success, stdout, stderr = execute_applescript(applescript) + + if not success: + return { + "status": "error", + "error": "Failed to execute AppleScript", + "details": stderr + } + + # Parse the JSON-like response + import re + result_str = stdout.strip() + + # Extract values using regex since it's a simple JSON-like format + found_match = re.search(r'"found":(\w+)', result_str) + if not found_match or found_match.group(1) != 'true': + error_match = re.search(r'"error":"([^"]*)"', result_str) + error_msg = error_match.group(1) if error_match else "iPhone Mirroring app not found" + return { + "status": "error", + "error": "iPhone Mirroring app not found or not active", + "details": error_msg, + "suggestion": "Please make sure iPhone Mirroring app is open and visible" + } + + # Extract window coordinates and size + x_match = re.search(r'"x":(\d+)', result_str) + y_match = re.search(r'"y":(\d+)', result_str) + width_match = re.search(r'"width":(\d+)', result_str) + height_match = re.search(r'"height":(\d+)', result_str) + + if not all([x_match, y_match, width_match, height_match]): + return { + "status": "error", + "error": "Failed to parse window dimensions", + "raw_output": result_str + } + + window_x = int(x_match.group(1)) + window_y = int(y_match.group(1)) + window_width = int(width_match.group(1)) + window_height = int(height_match.group(1)) + + # Extract app info if available + app_match = re.search(r'"app":"([^"]*)"', result_str) + bundle_match = re.search(r'"bundle":"([^"]*)"', result_str) + app_name = app_match.group(1) if app_match else "Unknown" + bundle_id = bundle_match.group(1) if bundle_match else "Unknown" + + print(f"🔍 Detected {app_name} window: {window_width}x{window_height} at ({window_x}, {window_y})") + print(f" Bundle ID: {bundle_id}") + + # Calculate device content area with smart detection based on window size + # Different calculation strategies based on window size + if window_width < 500 and window_height < 1000: + # Small window - minimal padding + title_bar_height = 28 + content_padding_h = 20 # horizontal padding + content_padding_v = 20 # vertical padding + elif window_width < 800 and window_height < 1400: + # Medium window - moderate padding + title_bar_height = 28 + content_padding_h = 40 + content_padding_v = 50 + else: + # Large window - more padding + title_bar_height = 28 + content_padding_h = 80 + content_padding_v = 100 + + # Calculate the actual iOS device screen area within the window + content_x = window_x + content_padding_h // 2 + content_y = window_y + title_bar_height + content_padding_v // 2 + content_width = window_width - content_padding_h + content_height = window_height - title_bar_height - content_padding_v + + # Ensure minimum viable dimensions + if content_width < 200 or content_height < 400: + # Try with minimal padding if initial calculation is too small + content_x = window_x + 10 + content_y = window_y + title_bar_height + 10 + content_width = window_width - 20 + content_height = window_height - title_bar_height - 20 + + if content_width < 200 or content_height < 400: + return { + "status": "error", + "error": "Detected window seems too small for iPhone content", + "window_size": [window_width, window_height], + "calculated_content": [content_width, content_height], + "suggestion": "Try making the iPhone Mirroring window larger" + } + + # Auto-configure the mapping + setup_ios_mapping(content_x, content_y, content_width, content_height) + + # Verify configuration was set + print(f"✅ iOS mapping auto-configured successfully!") + print(f" Enabled: {ios_config['enabled']}") + print(f" Device estimation: {ios_config['estimated_ios_width']}x{ios_config['estimated_ios_height']}") + print(f" Mirror area: {ios_config['mirror_width']}x{ios_config['mirror_height']} at ({ios_config['mirror_x']}, {ios_config['mirror_y']})") + + return { + "status": "ok", + "action": "detect_ios_mirror", + "window_detected": { + "x": window_x, + "y": window_y, + "width": window_width, + "height": window_height, + "app_name": app_name, + "bundle_id": bundle_id + }, + "content_area": { + "x": content_x, + "y": content_y, + "width": content_width, + "height": content_height + }, + "config": ios_config, + "message": f"Successfully auto-configured for {ios_config['estimated_ios_width']}x{ios_config['estimated_ios_height']} device" + } + + except Exception as e: + return { + "status": "error", + "error": f"Exception during auto-detection: {str(e)}", + "traceback": traceback.format_exc() + } + +def execute_applescript(script): + """Execute AppleScript command""" + try: + result = subprocess.run(['osascript', '-e', script], + capture_output=True, text=True, timeout=5) + return result.returncode == 0, result.stdout, result.stderr + except Exception as e: + return False, "", str(e) + +# iOS device configuration +ios_config = { + "enabled": False, + "mirror_x": 0, + "mirror_y": 0, + "mirror_width": 0, + "mirror_height": 0, + "ios_aspect_ratio": 2.17, # Default iPhone ratio (852/393) + "estimated_ios_width": 393, + "estimated_ios_height": 852 +} + +def setup_ios_mapping(mirror_x, mirror_y, mirror_width, mirror_height): + """Setup coordinate mapping for iOS device mirroring""" + global ios_config + + # Estimate iOS device dimensions based on mirror aspect ratio + mirror_aspect_ratio = mirror_height / mirror_width + + # Common iOS device configurations + ios_devices = [ + {"name": "iPhone 15 Pro", "width": 393, "height": 852}, + {"name": "iPhone 15 Plus", "width": 428, "height": 926}, + {"name": "iPhone 12/13/14", "width": 390, "height": 844}, + {"name": "iPhone 11 Pro Max", "width": 414, "height": 896}, + {"name": "iPhone X/XS", "width": 375, "height": 812}, + {"name": "iPad Pro 12.9", "width": 1024, "height": 1366}, + {"name": "iPad Pro 11", "width": 834, "height": 1194}, + ] + + # Find closest matching device based on aspect ratio + best_match = min(ios_devices, key=lambda d: abs((d["height"] / d["width"]) - mirror_aspect_ratio)) + + ios_config.update({ + "enabled": True, + "mirror_x": mirror_x, + "mirror_y": mirror_y, + "mirror_width": mirror_width, + "mirror_height": mirror_height, + "ios_aspect_ratio": mirror_aspect_ratio, + "estimated_ios_width": best_match["width"], + "estimated_ios_height": best_match["height"] + }) + + print(f"📱 iOS mapping configured: Estimated {best_match['name']} ({best_match['width']}x{best_match['height']}) -> {mirror_width}x{mirror_height} at ({mirror_x},{mirror_y})") + print(f" Aspect ratio: {mirror_aspect_ratio:.3f}, Device: {best_match['name']}") + print(f" ✅ iOS coordinate transformation is now ENABLED") + +def transform_ios_coordinates(ios_x, ios_y): + """Transform iOS coordinates to macOS screen coordinates""" + if not ios_config["enabled"]: + return ios_x, ios_y + + # Calculate scale factors based on estimated iOS dimensions + scale_x = ios_config["mirror_width"] / ios_config["estimated_ios_width"] + scale_y = ios_config["mirror_height"] / ios_config["estimated_ios_height"] + + # Convert iOS coordinates to macOS coordinates + mac_x = ios_config["mirror_x"] + (ios_x * scale_x) + mac_y = ios_config["mirror_y"] + (ios_y * scale_y) + + return int(mac_x), int(mac_y) + +def get_ios_screenshot_region(): + """Get the region for iOS device screenshot""" + if not ios_config["enabled"]: + return None + + return ( + ios_config["mirror_x"], + ios_config["mirror_y"], + ios_config["mirror_width"], + ios_config["mirror_height"] + ) + +def handle_action(action): + try: + act = action.get("action") + if act == "click": + x = int(action["x"]) + y = int(action["y"]) + # Transform coordinates if iOS mapping is enabled + mac_x, mac_y = transform_ios_coordinates(x, y) + + # Validate coordinates are within expected iOS mirror region + if ios_config["enabled"]: + mirror_left = ios_config["mirror_x"] + mirror_top = ios_config["mirror_y"] + mirror_right = mirror_left + ios_config["mirror_width"] + mirror_bottom = mirror_top + ios_config["mirror_height"] + + if not (mirror_left <= mac_x <= mirror_right and mirror_top <= mac_y <= mirror_bottom): + print(f"WARNING: Click coordinates ({mac_x}, {mac_y}) are outside iOS mirror region ({mirror_left}, {mirror_top}, {mirror_right}, {mirror_bottom})") + print(f"Original iOS coordinates: ({x}, {y})") + print(f"This might cause the iOS app to lose focus!") + + print(f"Clicking at iOS coords ({x}, {y}) -> macOS coords ({mac_x}, {mac_y})") + + pyautogui.click(mac_x, mac_y) + return {"status": "ok", "action": "click", "ios_coords": [x, y], "mac_coords": [mac_x, mac_y]} + + elif act == "move": + x = int(action["x"]) + y = int(action["y"]) + duration = float(action.get("duration", 0.2)) + # Transform coordinates if iOS mapping is enabled + mac_x, mac_y = transform_ios_coordinates(x, y) + pyautogui.moveTo(mac_x, mac_y, duration=duration) + return {"status": "ok", "action": "move", "ios_coords": [x, y], "mac_coords": [mac_x, mac_y]} + + elif act == "drag": + x = int(action["x"]) + y = int(action["y"]) + x2 = int(action["x2"]) + y2 = int(action["y2"]) + duration = float(action.get("duration", 0.5)) + # Transform coordinates if iOS mapping is enabled + mac_x, mac_y = transform_ios_coordinates(x, y) + mac_x2, mac_y2 = transform_ios_coordinates(x2, y2) + pyautogui.moveTo(mac_x, mac_y) + pyautogui.dragTo(mac_x2, mac_y2, duration=duration) + return {"status": "ok", "action": "drag", "ios_from": [x, y], "ios_to": [x2, y2], "mac_from": [mac_x, mac_y], "mac_to": [mac_x2, mac_y2]} + + elif act == "type": + # select all + pyautogui.hotkey('command', 'a') + text = action["text"] + interval = float(action.get("interval", 0.0)) + # For iOS, we need slower typing to ensure proper character registration + # iOS virtual keyboards can miss characters if typing is too fast + if interval == 0.0: + # Set a default interval for iOS compatibility + interval = 0.02 # 20ms between characters - good balance for iOS + + print(f"📱 iOS Type: '{text}' with interval {interval}s") + + # Use AppleScript to simulate keyboard input to avoid system shortcuts + # This method sends text directly to the active application without triggering shortcuts + try: + # Escape special characters for AppleScript + escaped_text = text.replace('"', '\\"').replace('\\', '\\\\') + + applescript = f''' + tell application "System Events" + keystroke "{escaped_text}" + end tell + ''' + + success, stdout, stderr = execute_applescript(applescript) + if success: + print(f" ✅ Used AppleScript keystroke for text input") + # Add interval delay if specified + if interval > 0: + time.sleep(len(text) * interval) + return {"status": "ok", "action": "type", "text": text, "method": "applescript", "interval": interval} + else: + print(f" ❌ AppleScript failed: {stderr}, falling back to character-by-character") + + except Exception as e: + print(f" ❌ AppleScript method failed: {e}, falling back to character-by-character") + + # Fallback: Character-by-character input with modifier key clearing + print(f" 🔤 Using character-by-character input method") + + # Clear any pressed modifier keys first + modifier_keys = ['shift', 'ctrl', 'alt', 'cmd'] + for key in modifier_keys: + try: + pyautogui.keyUp(key) + except: + pass # Ignore if key wasn't pressed + + # Type each character individually + for i, char in enumerate(text): + try: + # For special characters that might cause issues, use write method + if char in [' ', '\n', '\t']: + if char == ' ': + pyautogui.press('space') + elif char == '\n': + pyautogui.press('enter') + elif char == '\t': + pyautogui.press('tab') + else: + # Use write for individual characters to avoid shortcut combinations + pyautogui.write(char) + + # Add interval delay between characters + if interval > 0 and i < len(text) - 1: + time.sleep(interval) + + except Exception as char_error: + print(f" ⚠️ Error typing character '{char}': {char_error}") + continue + + return {"status": "ok", "action": "type", "text": text, "method": "character_by_character", "interval": interval} + + elif act == "key": + key = action["key"] + pyautogui.press(key) + return {"status": "ok", "action": "key", "key": key} + + elif act == "hotkey": + keys = action["keys"] + if isinstance(keys, list): + pyautogui.hotkey(*keys) + else: + pyautogui.hotkey(keys) + return {"status": "ok", "action": "hotkey", "keys": keys} + + elif act == "scroll": + x = int(action.get("x", ios_config["estimated_ios_width"] // 2 if ios_config["enabled"] else pyautogui.size().width // 2)) + y = int(action.get("y", ios_config["estimated_ios_height"] // 2 if ios_config["enabled"] else pyautogui.size().height // 2)) + + # Enhanced distance calculation for better Android compatibility + distance = int(action.get("distance", 100)) + direction = action.get("direction", "down") # up, down, left, right + + # Transform coordinates if iOS mapping is enabled + mac_x, mac_y = transform_ios_coordinates(x, y) + + # Calculate clicks based on distance + if distance <= 50: + clicks = max(8, int(distance * 0.4)) + elif distance <= 150: + clicks = max(12, int(distance * 0.25)) + elif distance <= 300: + clicks = max(18, int(distance * 0.18)) + else: + clicks = max(25, int(distance * 0.12)) + + print(f"📍 SCROLL: iOS({x}, {y}) -> Mac({mac_x}, {mac_y}), Direction: {direction}, Distance: {distance}px, Clicks: {clicks}") + + # Move mouse to the target position first + pyautogui.moveTo(mac_x, mac_y) + + # Simplified scroll logic - direct implementation with multiple methods + if direction in ["left", "right"]: + print(f"🔄 HORIZONTAL SCROLL: {direction}") + method = "horizontal_scroll" + success = False + + if hasattr(pyautogui, 'hscroll'): + for i in range(clicks): + scroll_amount = 20 if direction == "left" else -20 + pyautogui.hscroll(scroll_amount, x=mac_x, y=mac_y) + else: + raise NotImplementedError("Horizontal scrolling not supported on this platform") + + else: + print(f"⬆️⬇️ VERTICAL SCROLL: {direction}") + # Vertical scrolling (this should work fine) + for i in range(clicks): + scroll_amount = 20 if direction == "up" else -20 + pyautogui.scroll(scroll_amount, x=mac_x, y=mac_y) + method = "vertical_scroll" + + print(f"✅ Scroll completed: {direction} ({clicks} iterations)") + return {"status": "ok", "action": "scroll", "method": method, "ios_coords": [x, y], "mac_coords": [mac_x, mac_y], "direction": direction, "clicks": clicks, "distance": distance} + + elif act == "screenshot": + # Take screenshot of iOS region if mapping is enabled + region = get_ios_screenshot_region() + print(f"📸 Taking screenshot with region: {region}") + + if region: + print(f" 📱 iOS screenshot region: x={region[0]}, y={region[1]}, w={region[2]}, h={region[3]}") + screenshot = pyautogui.screenshot(region=region) + else: + print(f" 🖥️ Full screen screenshot (iOS config not enabled)") + screenshot = pyautogui.screenshot() + + # Convert screenshot to base64 for web frontend + buffer = io.BytesIO() + screenshot.save(buffer, format='PNG') + buffer.seek(0) + + # Create base64 data URL + img_base64 = base64.b64encode(buffer.read()).decode('utf-8') + data_url = f"data:image/png;base64,{img_base64}" + + # Also save to temporary file as backup + temp_path = f"/tmp/screenshot_{int(time.time())}.png" + screenshot.save(temp_path) + + print(f" ✅ Screenshot saved: {temp_path}") + print(f" 📊 Screenshot size: {screenshot.size[0]}x{screenshot.size[1]}") + + return { + "status": "ok", + "action": "screenshot", + "path": temp_path, + "data_url": data_url, + "ios_region": region is not None, + "region_info": region if region else None, + "screenshot_size": {"width": screenshot.size[0], "height": screenshot.size[1]} + } + + elif act == "get_screen_size": + if ios_config["enabled"]: + return {"status": "ok", "action": "get_screen_size", "width": ios_config["estimated_ios_width"], "height": ios_config["estimated_ios_height"], "mode": "ios"} + else: + size = pyautogui.size() + return {"status": "ok", "action": "get_screen_size", "width": size.width, "height": size.height, "mode": "mac"} + + elif act == "configure_ios": + mirror_x = int(action["mirror_x"]) + mirror_y = int(action["mirror_y"]) + mirror_width = int(action["mirror_width"]) + mirror_height = int(action["mirror_height"]) + setup_ios_mapping(mirror_x, mirror_y, mirror_width, mirror_height) + return {"status": "ok", "action": "configure_ios", "config": ios_config} + + elif act == "detect_ios_mirror": + """Automatically detect iPhone Mirroring app window and configure mapping""" + result = detect_and_configure_ios_mirror() + return result + + elif act == "sleep": + seconds = float(action["seconds"]) + time.sleep(seconds) + return {"status": "ok", "action": "sleep", "seconds": seconds} + + else: + return {"status": "error", "error": f"Unknown action: {act}"} + + except Exception as e: + return { + "status": "error", + "error": str(e), + "traceback": traceback.format_exc() + } + +@app.route("/health", methods=["GET"]) +def health_check(): + """Health check endpoint""" + try: + screen_size = pyautogui.size() + return jsonify({ + "status": "ok", + "message": "PyAutoGUI server is running", + "screen_size": {"width": screen_size.width, "height": screen_size.height}, + "pyautogui_version": pyautogui.__version__ if hasattr(pyautogui, '__version__') else "unknown" + }) + except Exception as e: + return jsonify({ + "status": "error", + "error": str(e) + }), 500 + +@app.route("/detect", methods=["POST"]) +def detect_ios_mirror(): + """Auto-detect and configure iOS device mapping""" + try: + result = handle_action({"action": "detect_ios_mirror"}) + return jsonify(result) + except Exception as e: + return jsonify({ + "status": "error", + "error": str(e), + "traceback": traceback.format_exc() + }) + +@app.route("/configure", methods=["POST"]) +def configure_ios(): + """Configure iOS device mapping""" + try: + data = request.get_json() + # Support both snake_case and camelCase naming + mirror_x = data.get("mirror_x") or data.get("mirrorX") + mirror_y = data.get("mirror_y") or data.get("mirrorY") + mirror_width = data.get("mirror_width") or data.get("mirrorWidth") + mirror_height = data.get("mirror_height") or data.get("mirrorHeight") + + result = handle_action({ + "action": "configure_ios", + "mirror_x": mirror_x, + "mirror_y": mirror_y, + "mirror_width": mirror_width, + "mirror_height": mirror_height + }) + return jsonify(result) + except Exception as e: + return jsonify({ + "status": "error", + "error": str(e), + "traceback": traceback.format_exc() + }) + +@app.route("/config", methods=["GET"]) +def get_config(): + """Get current iOS configuration""" + return jsonify({ + "status": "ok", + "config": ios_config + }) + +@app.route("/run", methods=["POST"]) +def run_actions(): + try: + data = request.get_json() + if isinstance(data, list): + results = [handle_action(act) for act in data] + return jsonify({"status": "done", "results": results}) + elif isinstance(data, dict): + result = handle_action(data) + return jsonify(result) + else: + return jsonify({"status": "error", "error": "Invalid input format"}) + except Exception as e: + return jsonify({ + "status": "error", + "error": str(e), + "traceback": traceback.format_exc() + }) + +if __name__ == "__main__": + port = int(sys.argv[1]) if len(sys.argv) > 1 else 1412 + print(f"Starting PyAutoGUI server on port {port}") + print(f"Screen size: {pyautogui.size()}") + print("Health check available at: http://localhost:{}/health".format(port)) + app.run(host="0.0.0.0", port=port, debug=False) \ No newline at end of file diff --git a/packages/ios/modern.config.ts b/packages/ios/modern.config.ts new file mode 100644 index 000000000..db0213ece --- /dev/null +++ b/packages/ios/modern.config.ts @@ -0,0 +1,16 @@ +import { defineConfig, moduleTools } from '@modern-js/module-tools'; + +export default defineConfig({ + plugins: [moduleTools()], + buildPreset: 'npm-library', + buildConfig: { + input: { + index: './src/index.ts', + agent: './src/agent/index.ts', + }, + target: 'es2020', + dts: { + respectExternal: true, + }, + }, +}); diff --git a/packages/ios/package.json b/packages/ios/package.json new file mode 100644 index 000000000..6fdcc0474 --- /dev/null +++ b/packages/ios/package.json @@ -0,0 +1,56 @@ +{ + "name": "@midscene/ios", + "version": "0.1.0", + "description": "Midscene.js for iOS automation", + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", + "exports": { + ".": { + "types": "./dist/lib/index.d.ts", + "import": "./dist/es/index.js", + "require": "./dist/lib/index.js" + }, + "./agent": { + "types": "./dist/lib/agent.d.ts", + "import": "./dist/es/agent.js", + "require": "./dist/lib/agent.js" + } + }, + "bin": { + "midscene-ios-server": "./bin/server.js" + }, + "files": ["lib/**/*", "bin/**/*", "idb/**/*", "README.md"], + "scripts": { + "build": "modern build", + "dev": "modern dev", + "test": "vitest", + "server": "node bin/server.js", + "setup": "./setup.sh", + "prepack": "npm run build" + }, + "dependencies": { + "@midscene/core": "workspace:*", + "@midscene/shared": "workspace:*", + "@midscene/web": "workspace:*", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@modern-js/module-tools": "^2.60.3", + "@types/node": "^22.10.5", + "dotenv": "^16.4.5", + "tsx": "^4.17.0", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "peerDependencies": {}, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/web-infra-dev/midscene.git", + "directory": "packages/ios" + }, + "license": "MIT" +} diff --git a/packages/ios/scripts/getAppWindowRect.scpt b/packages/ios/scripts/getAppWindowRect.scpt new file mode 100644 index 000000000..fc0b35ecd --- /dev/null +++ b/packages/ios/scripts/getAppWindowRect.scpt @@ -0,0 +1,30 @@ +delay 4 -- you have 4 seconds to make iPhone Mirroring App foreground!! + +tell application "System Events" + set frontApp to name of first application process whose frontmost is true + tell application process frontApp + set win to first window + set pos to position of win + set size_ to size of win + + -- set margrin + set leftMargin to 6 + set rightMargin to 6 + set topMargin to 38 + set bottomMargin to 6 + + -- original + set originalX to item 1 of pos + set originalY to item 2 of pos + set originalWidth to item 1 of size_ + set originalHeight to item 2 of size_ + + -- clipped + set contentX to originalX + leftMargin + set contentY to originalY + topMargin + set contentWidth to originalWidth - leftMargin - rightMargin + set contentHeight to originalHeight - topMargin - bottomMargin + + return {frontApp, pos, size_, {contentX, contentY, contentWidth, contentHeight}} + end tell +end tell \ No newline at end of file diff --git a/packages/ios/setup.sh b/packages/ios/setup.sh new file mode 100755 index 000000000..5a75b1cc2 --- /dev/null +++ b/packages/ios/setup.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +echo "Setting up iOS package dependencies..." + +# Check if Python 3 is installed +if ! command -v python3 &> /dev/null; then + echo "❌ Python 3 is not installed. Please install Python 3 first." + exit 1 +fi + +echo "✅ Python 3 found: $(python3 --version)" + +# Check if pip3 is available +if ! command -v pip3 &> /dev/null; then + echo "❌ pip3 is not available. Please install pip3 first." + exit 1 +fi + +echo "✅ pip3 found: $(pip3 --version)" + +# Install required Python packages +echo "Installing Python dependencies..." +pip3 install flask pyautogui pillow requests + +echo "✅ Python dependencies installed" + +# Make the server script executable +chmod +x bin/server.js + +echo "✅ iOS package setup completed!" +echo "" +echo "To test the setup:" +echo "1. Start the server: npm run server" +echo "2. Run example: npm run example" diff --git a/packages/ios/src/agent/index.ts b/packages/ios/src/agent/index.ts new file mode 100644 index 000000000..2ebf5b9c4 --- /dev/null +++ b/packages/ios/src/agent/index.ts @@ -0,0 +1,55 @@ +import { vlLocateMode } from '@midscene/shared/env'; +import { PageAgent, type PageAgentOpt } from '@midscene/web/agent'; +import { iOSDevice, type iOSDeviceOpt } from '../page'; +import { debugPage } from '../page'; +import { getScreenSize, startPyAutoGUIServer } from '../utils'; + +type iOSAgentOpt = PageAgentOpt; + +export class iOSAgent extends PageAgent { + declare page: iOSDevice; + private connectionPromise: Promise | null = null; + + constructor(page: iOSDevice, opts?: iOSAgentOpt) { + super(page, opts); + this.ensureConnected(); + } + + private ensureConnected(): Promise { + if (!this.connectionPromise) { + this.connectionPromise = this.page.connect(); + } + return this.connectionPromise; + } + +} + +export async function agentFromPyAutoGUI(opts?: iOSAgentOpt & iOSDeviceOpt) { + // Start PyAutoGUI server if not already running + const serverPort = opts?.serverPort || 1412; + + try { + // Try to test if server is already running + const fetch = (await import('node-fetch')).default; + await fetch(`http://localhost:${serverPort}/run`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'sleep', seconds: 0 }), + }); + console.log(`PyAutoGUI server is already running on port ${serverPort}`); + } catch (error) { + console.log(`Starting PyAutoGUI server on port ${serverPort}...`); + await startPyAutoGUIServer(serverPort); + } + + const page = new iOSDevice({ + serverUrl: opts?.serverUrl, + serverPort, + autoDismissKeyboard: opts?.autoDismissKeyboard, + mirrorConfig: opts?.mirrorConfig, + }); + + await page.connect(); + + return new iOSAgent(page, opts); +} diff --git a/packages/ios/src/index.ts b/packages/ios/src/index.ts new file mode 100644 index 000000000..f2a459a1b --- /dev/null +++ b/packages/ios/src/index.ts @@ -0,0 +1,4 @@ +export { iOSDevice } from './page'; +export { iOSAgent, agentFromPyAutoGUI } from './agent'; +export { getScreenSize } from './utils'; +export { overrideAIConfig } from '@midscene/shared/env'; diff --git a/packages/ios/src/page/index.ts b/packages/ios/src/page/index.ts new file mode 100644 index 000000000..6205fdeba --- /dev/null +++ b/packages/ios/src/page/index.ts @@ -0,0 +1,1222 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { Point, Size } from '@midscene/core'; +import type { DeviceAction, ExecutorContext, PageType } from '@midscene/core'; +import { getTmpFile, sleep } from '@midscene/core/utils'; +import type { ElementInfo } from '@midscene/shared/extractor'; +import { resizeImg } from '@midscene/shared/img'; +import { getDebug } from '@midscene/shared/logger'; +import { + type AndroidDeviceInputOpt, + type AndroidDevicePage, +} from '@midscene/web'; +import { commonWebActionsForWebPage } from '@midscene/web/utils'; +import { type ScreenInfo, getScreenSize } from '../utils'; + +export const debugPage = getDebug('ios:device'); +export interface iOSDeviceOpt extends AndroidDeviceInputOpt { + serverUrl?: string; + serverPort?: number; + autoDismissKeyboard?: boolean; + // iOS device mirroring configuration + mirrorConfig?: { + mirrorX: number; + mirrorY: number; + mirrorWidth: number; + mirrorHeight: number; + }; +} + +export interface PyAutoGUIAction { + action: + | 'click' + | 'move' + | 'drag' + | 'type' + | 'key' + | 'hotkey' + | 'sleep' + | 'screenshot' + | 'scroll'; + x?: number; + y?: number; + x2?: number; + y2?: number; + text?: string; + key?: string; + keys?: string[]; + seconds?: number; + direction?: 'up' | 'down' | 'left' | 'right'; + clicks?: number; + distance?: number; // Original scroll distance in pixels + scroll_type?: 'wheel' | 'trackpad'; + interval?: number; // Interval between keystrokes for type action +} + +export interface PyAutoGUIResult { + status: 'ok' | 'error'; + action?: string; + x?: number; + y?: number; + text?: string; + seconds?: number; + from?: [number, number]; + to?: [number, number]; + path?: string; // For screenshot action + ios_region?: boolean; // For screenshot action + direction?: string; // For scroll action + clicks?: number; // For scroll action + method?: string; // For scroll action (wheel, trackpad, etc.) + ios_coords?: [number, number]; // For coordinate transformation info + mac_coords?: [number, number]; // For coordinate transformation info + error?: string; + traceback?: string; +} + +export class iOSDevice implements AndroidDevicePage { + private devicePixelRatio = 1; + private screenInfo: ScreenInfo | null = null; + private destroyed = false; + pageType: PageType = 'ios'; + uri: string | undefined; + options?: iOSDeviceOpt; + private serverUrl: string; + private serverPort: number; + private serverProcess?: any; // Store reference to server process + + constructor(options?: iOSDeviceOpt) { + this.options = options; + this.serverPort = options?.serverPort || 1412; + this.serverUrl = + options?.serverUrl || `http://localhost:${this.serverPort}`; + } + + actionSpace(): DeviceAction[] { + const commonActions = commonWebActionsForWebPage(this); + commonActions.forEach((action) => { + if (action.name === 'Input') { + action.call = async (context, param) => { + const { element } = context; + if (element) { + await this.clearInput(element as unknown as ElementInfo); + + if (!param || !param.value) { + return; + } + } + + await this.keyboard.type(param.value, { + autoDismissKeyboard: + param.autoDismissKeyboard ?? this.options?.autoDismissKeyboard, + }); + }; + } + }); + + const allActions: DeviceAction[] = [ + ...commonWebActionsForWebPage(this), + { + name: 'IOSBackButton', + description: 'Trigger the system "back" operation on iOS devices', + location: false, + call: async (context, param) => { + await this.back(); + }, + }, + { + name: 'IOSHomeButton', + description: 'Trigger the system "home" operation on iOS devices', + location: false, + call: async (context, param) => { + await this.home(); + }, + }, + { + name: 'IOSRecentAppsButton', + description: + 'Trigger the system "recent apps" operation on iOS devices', + location: false, + call: async (context, param) => { + await this.recentApps(); + }, + }, + { + name: 'IOSLongPress', + description: + 'Trigger a long press on the screen at specified coordinates on iOS devices', + paramSchema: '{ duration?: number }', + paramDescription: 'The duration of the long press in milliseconds', + location: 'required', + whatToLocate: 'The element to be long pressed', + call: async (context, param) => { + const { element } = context; + if (!element) { + throw new Error( + 'IOSLongPress requires an element to be located', + ); + } + const [x, y] = element.center; + await this.longPress(x, y, param?.duration); + }, + } as DeviceAction<{ duration?: number }>, + { + name: 'IOSPull', + description: + 'Trigger pull down to refresh or pull up actions on iOS devices', + paramSchema: + '{ direction: "up" | "down", distance?: number, duration?: number }', + paramDescription: + 'The direction to pull, the distance to pull (in pixels), and the duration of the pull (in milliseconds).', + location: 'optional', + whatToLocate: 'The element to be pulled', + call: async (context, param) => { + const { element } = context; + const startPoint = element + ? { left: element.center[0], top: element.center[1] } + : undefined; + if (!param || !param.direction) { + throw new Error('IOSPull requires a direction parameter'); + } + if (param.direction === 'down') { + await this.pullDown(startPoint, param.distance, param.duration); + } else if (param.direction === 'up') { + await this.pullUp(startPoint, param.distance, param.duration); + } else { + throw new Error(`Unknown pull direction: ${param.direction}`); + } + }, + } as DeviceAction<{ + direction: 'up' | 'down'; + distance?: number; + duration?: number; + }>, + ]; + return allActions; + } + + + public async connect(): Promise { + if (this.destroyed) { + throw new Error('iOSDevice has been destroyed and cannot be used'); + } + + // Health check to ensure Python server is running + try { + const response = await fetch(`${this.serverUrl}/health`); + if (!response.ok) { + throw new Error( + `Python server health check failed: ${response.status}`, + ); + } + const healthData = await response.json(); + debugPage(`Python server is running: ${JSON.stringify(healthData)}`); + } catch (error: any) { + debugPage(`Python server connection failed: ${error.message}`); + + // Try to start server automatically + debugPage('Attempting to start Python server automatically...'); + + try { + await this.startPyAutoGUIServer(); + debugPage('Python server started successfully'); + + // Verify server is now running + const response = await fetch(`${this.serverUrl}/health`); + if (!response.ok) { + throw new Error(`Server still not responding after startup: ${response.status}`); + } + + const healthData = await response.json(); + debugPage(`Python server is now running: ${JSON.stringify(healthData)}`); + } catch (startError: any) { + throw new Error( + `Failed to auto-start Python server: ${startError.message}. ` + + `Please manually start the server by running: node packages/ios/bin/server.js ${this.serverPort}` + ); + } + } + + // Make iPhone mirroring app foreground + try { + // Use fixed mirroring app name for iOS device screen mirroring + const mirroringAppName = 'iPhone Mirroring'; + + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + // Activate the mirroring application using AppleScript + await execAsync( + `osascript -e 'tell application "${mirroringAppName}" to activate'`, + ); + debugPage(`Activated iOS mirroring app: ${mirroringAppName}`); + + //wait for app to be ready + await sleep(2000); + } catch (mirrorError: any) { + debugPage( + `Warning: Failed to bring iOS mirroring app to foreground: ${mirrorError.message}`, + ); + // Continue execution even if this fails - it's not critical + } + + // Configure iOS mirroring if provided + await this.initializeMirrorConfiguration(); + + } + + private async startPyAutoGUIServer(): Promise { + try { + const { spawn } = await import('node:child_process'); + const serverScriptPath = path.resolve(__dirname, '../../bin/server.js'); + + debugPage(`Starting PyAutoGUI server using: node ${serverScriptPath} ${this.serverPort}`); + + // Start server process in background (similar to server.js background mode) + this.serverProcess = spawn('node', [serverScriptPath, this.serverPort.toString()], { + detached: true, + stdio: 'pipe', // Capture output + env: { + ...process.env, + }, + }); + + // Handle server process events + this.serverProcess.on('error', (error: any) => { + debugPage(`Server process error: ${error.message}`); + }); + + this.serverProcess.on('exit', (code: number, signal: string) => { + debugPage(`Server process exited with code ${code}, signal ${signal}`); + }); + + // Capture and log server output + if (this.serverProcess.stdout) { + this.serverProcess.stdout.on('data', (data: Buffer) => { + debugPage(`Server stdout: ${data.toString().trim()}`); + }); + } + + if (this.serverProcess.stderr) { + this.serverProcess.stderr.on('data', (data: Buffer) => { + debugPage(`Server stderr: ${data.toString().trim()}`); + }); + } + + debugPage(`Started PyAutoGUI server process with PID: ${this.serverProcess.pid}`); + + // Wait for server to start up (similar to server.js timeout) + await sleep(3000); + + } catch (error: any) { + throw new Error(`Failed to start PyAutoGUI server: ${error.message}`); + } + } + + private async initializeMirrorConfiguration() { + if (this.options?.mirrorConfig) { + await this.configureIOSMirror(this.options.mirrorConfig); + } else { + try { + // Auto-detect iPhone Mirroring app window using AppleScript + const mirrorConfig = await this.detectAndConfigureIOSMirror(); + if (mirrorConfig) { + if (!this.options || typeof this.options.mirrorConfig !== 'object') { + this.options = {}; + } + this.options.mirrorConfig = mirrorConfig; + + debugPage( + `Auto-detected iOS mirror config: ${mirrorConfig.mirrorWidth}x${mirrorConfig.mirrorHeight} at (${mirrorConfig.mirrorX}, ${mirrorConfig.mirrorY})` + ); + + // Configure the detected mirror settings + await this.configureIOSMirror(mirrorConfig); + } else { + debugPage('No iPhone Mirroring app found or auto-detection failed'); + } + } catch (error: any) { + debugPage(`Failed to auto-detect iPhone Mirroring app: ${error.message}`); + } + } + + // Get screen information (will use iOS dimensions if configured) + this.screenInfo = await getScreenSize(); + this.devicePixelRatio = this.screenInfo.dpr; + + debugPage( + `iOS Device initialized - Screen: ${this.screenInfo.width}x${this.screenInfo.height}, DPR: ${this.devicePixelRatio}`, + ); + } + + private async configureIOSMirror(config: { + mirrorX: number; + mirrorY: number; + mirrorWidth: number; + mirrorHeight: number; + }): Promise { + try { + const response = await fetch(`${this.serverUrl}/configure`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config), + }); + + if (!response.ok) { + throw new Error(`Failed to configure iOS mirror: ${response.status}`); + } + + const result = await response.json(); + if (result.status !== 'ok') { + throw new Error(`iOS configuration failed: ${result.error}`); + } + + debugPage( + `iOS mirroring configured: mirror region ${config.mirrorX},${config.mirrorY} -> ${config.mirrorWidth}x${config.mirrorHeight}`, + ); + } catch (error: any) { + throw new Error(`Failed to configure iOS mirroring: ${error.message}`); + } + } + + private async detectAndConfigureIOSMirror(): Promise<{ + mirrorX: number; + mirrorY: number; + mirrorWidth: number; + mirrorHeight: number; + } | null> { + try { + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + // AppleScript to get window information for iPhone Mirroring app + const applescript = ` + tell application "System Events" + try + set mirrorApp to first application process whose name contains "iPhone Mirroring" + set mirrorWindow to first window of mirrorApp + set windowPosition to position of mirrorWindow + set windowSize to size of mirrorWindow + + -- Get window frame information + set windowX to item 1 of windowPosition + set windowY to item 2 of windowPosition + set windowWidth to item 1 of windowSize + set windowHeight to item 2 of windowSize + + -- Try to get the actual visible frame (content area) + try + set appName to name of mirrorApp + set bundleId to bundle identifier of mirrorApp + set visibleFrame to "{\\"found\\":true,\\"x\\":" & windowX & ",\\"y\\":" & windowY & ",\\"width\\":" & windowWidth & ",\\"height\\":" & windowHeight & ",\\"app\\":\\"" & appName & "\\",\\"bundle\\":\\"" & bundleId & "\\"}" + return visibleFrame + on error + return "{\\"found\\":true,\\"x\\":" & windowX & ",\\"y\\":" & windowY & ",\\"width\\":" & windowWidth & ",\\"height\\":" & windowHeight & "}" + end try + + on error errMsg + return "{\\"found\\":false,\\"error\\":\\"" & errMsg & "\\"}" + end try + end tell + `; + + const { stdout, stderr } = await execAsync(`osascript -e '${applescript}'`); + + if (stderr) { + debugPage(`AppleScript error: ${stderr}`); + return null; + } + + const result = JSON.parse(stdout.trim()); + + if (!result.found) { + debugPage(`iPhone Mirroring app not found: ${result.error || 'Unknown error'}`); + return null; + } + + const windowX = result.x; + const windowY = result.y; + const windowWidth = result.width; + const windowHeight = result.height; + + debugPage(`Detected iPhone Mirroring window: ${windowWidth}x${windowHeight} at (${windowX}, ${windowY})`); + + // Calculate device content area with smart detection based on window size + let titleBarHeight = 28; + let contentPaddingH, contentPaddingV; + + if (windowWidth < 500 && windowHeight < 1000) { + // Small window - minimal padding + contentPaddingH = 20; + contentPaddingV = 20; + } else if (windowWidth < 800 && windowHeight < 1400) { + // Medium window - moderate padding + contentPaddingH = 40; + contentPaddingV = 50; + } else { + // Large window - more padding + contentPaddingH = 80; + contentPaddingV = 100; + } + + // Calculate the actual iOS device screen area within the window + const contentX = windowX + Math.floor(contentPaddingH / 2); + const contentY = windowY + titleBarHeight + Math.floor(contentPaddingV / 2); + const contentWidth = windowWidth - contentPaddingH; + const contentHeight = windowHeight - titleBarHeight - contentPaddingV; + + // Ensure minimum viable dimensions + if (contentWidth < 200 || contentHeight < 400) { + // Try with minimal padding if initial calculation is too small + const minimalContentX = windowX + 10; + const minimalContentY = windowY + titleBarHeight + 10; + const minimalContentWidth = windowWidth - 20; + const minimalContentHeight = windowHeight - titleBarHeight - 20; + + if (minimalContentWidth < 200 || minimalContentHeight < 400) { + debugPage(`Detected window seems too small for iPhone content: ${windowWidth}x${windowHeight}`); + return null; + } + + return { + mirrorX: minimalContentX, + mirrorY: minimalContentY, + mirrorWidth: minimalContentWidth, + mirrorHeight: minimalContentHeight, + }; + } + + debugPage(`Calculated content area: ${contentWidth}x${contentHeight} at (${contentX}, ${contentY})`); + + return { + mirrorX: contentX, + mirrorY: contentY, + mirrorWidth: contentWidth, + mirrorHeight: contentHeight, + }; + } catch (error: any) { + debugPage(`Exception during iPhone Mirroring app detection: ${error.message}`); + return null; + } + } + + async getConfiguration(): Promise { + const response = await fetch(`${this.serverUrl}/config`); + if (!response.ok) { + throw new Error(`Failed to get configuration: ${response.status}`); + } + return await response.json(); + } + + public async launch(uri: string): Promise { + this.uri = uri; + + try { + if (uri.startsWith('http://') || uri.startsWith('https://')) { + // Open URL in default browser + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + await execAsync(`open "${uri}"`); + debugPage(`Successfully launched URL: ${uri}`); + } else { + // Try to open as application + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + await execAsync(`open -a "${uri}"`); + debugPage(`Successfully launched app: ${uri}`); + } + } catch (error: any) { + debugPage(`Error launching ${uri}: ${error}`); + throw new Error(`Failed to launch ${uri}: ${error.message}`, { + cause: error, + }); + } + + return this; + } + + async size(): Promise { + // For iOS mirroring mode, return iOS device logical size instead of macOS screen size + if (this.options?.mirrorConfig) { + // Get configuration from Python server, using estimated iOS device size + try { + const config = await this.getConfiguration(); + if (config.status === 'ok' && config.config.enabled) { + return { + width: config.config.estimated_ios_width, + height: config.config.estimated_ios_height, + dpr: 1, // iOS coordinate system doesn't need additional pixel ratio adjustment + }; + } + } catch (error) { + debugPage('Failed to get iOS configuration, using fallback:', error); + } + } + + // Fallback for non-iOS mirroring mode or when configuration retrieval fails + if (!this.screenInfo) { + this.screenInfo = await getScreenSize(); + } + + return { + width: this.screenInfo.width, + height: this.screenInfo.height, + dpr: this.devicePixelRatio, + }; + } + + private adjustCoordinates(x: number, y: number): { x: number; y: number } { + const ratio = this.devicePixelRatio; + return { + x: Math.round(x * ratio), + y: Math.round(y * ratio), + }; + } + + private reverseAdjustCoordinates( + x: number, + y: number, + ): { x: number; y: number } { + const ratio = this.devicePixelRatio; + return { + x: Math.round(x / ratio), + y: Math.round(y / ratio), + }; + } + + async screenshotBase64(): Promise { + debugPage('screenshotBase64 begin'); + + try { + // Use PyAutoGUI server's screenshot functionality for iOS mirroring + if (this.options?.mirrorConfig) { + const result = await this.executePyAutoGUIAction({ + action: 'screenshot', + }); + + if (result.status === 'ok' && result.path) { + // Read the screenshot file and convert to base64 + const screenshotBuffer = await fs.promises.readFile(result.path); + + // Get iOS device dimensions for resizing + const { width, height } = await this.size(); + + // Resize to match iOS device dimensions + const resizedScreenshotBuffer = await resizeImg(screenshotBuffer, { + width, + height, + }); + + // Clean up temporary file + try { + await fs.promises.unlink(result.path); + } catch (cleanupError) { + debugPage('Failed to cleanup temp screenshot file:', cleanupError); + } + + debugPage('screenshotBase64 end (via PyAutoGUI server)'); + return `data:image/png;base64,${resizedScreenshotBuffer.toString('base64')}`; + } else { + throw new Error('PyAutoGUI screenshot failed: no path returned'); + } + } else { + // Fallback to macOS screencapture for non-mirroring scenarios + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + const tempPath = getTmpFile('png')!; + + // Use screencapture to take screenshot + await execAsync(`screencapture -x "${tempPath}"`); + + // Read and resize the screenshot + const screenshotBuffer = await fs.promises.readFile(tempPath); + const { width, height } = await this.size(); + + const resizedScreenshotBuffer = await resizeImg(screenshotBuffer, { + width, + height, + }); + + debugPage('screenshotBase64 end (via screencapture)'); + return `data:image/png;base64,${resizedScreenshotBuffer.toString('base64')}`; + } + } catch (error: any) { + debugPage('screenshotBase64 error:', error); + throw new Error(`Failed to take screenshot: ${error.message}`); + } + } + + /** + * Execute action via PyAutoGUI server + */ + private async executePyAutoGUIAction( + action: PyAutoGUIAction, + ): Promise { + try { + const fetch = (await import('node-fetch')).default; + + const response = await fetch(`${this.serverUrl}/run`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(action), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = (await response.json()) as PyAutoGUIResult; + + if (result.status === 'error') { + throw new Error(`PyAutoGUI error: ${result.error}`); + } + + return result; + } catch (error: any) { + debugPage('PyAutoGUI action failed:', error); + throw new Error(`Failed to execute PyAutoGUI action: ${error.message}`); + } + } + + async tap(point: Point): Promise { + debugPage(`tap at (${point.left}, ${point.top})`); + + if (this.options?.mirrorConfig) { + await this.executePyAutoGUIAction({ + action: 'click', + x: point.left, + y: point.top, + }); + } else { + const adjusted = this.adjustCoordinates(point.left, point.top); + await this.executePyAutoGUIAction({ + action: 'click', + x: adjusted.x, + y: adjusted.y, + }); + } + } + + async hover(point: Point): Promise { + debugPage(`hover at (${point.left}, ${point.top})`); + + if (this.options?.mirrorConfig) { + await this.executePyAutoGUIAction({ + action: 'move', + x: point.left, + y: point.top, + }); + } else { + const adjusted = this.adjustCoordinates(point.left, point.top); + await this.executePyAutoGUIAction({ + action: 'move', + x: adjusted.x, + y: adjusted.y, + }); + } + } + + async input(text: string, options?: AndroidDeviceInputOpt): Promise { + debugPage(`input text: ${text}`); + + // For iOS, we use the optimized type action with proper intervals + // The auto server will handle this appropriately for iOS + await this.executePyAutoGUIAction({ + action: 'type', + text, + interval: 0.05, // Proper interval for iOS keyboard responsiveness + }); + + // For iOS mirroring, default to NOT dismissing keyboard as it can cause issues + // Only dismiss if explicitly enabled + if ( + options?.autoDismissKeyboard === true || + this.options?.autoDismissKeyboard === true + ) { + await this.dismissKeyboard(); + } + } + + private async dismissKeyboard(): Promise { + try { + // Method 1: Try to tap the "Done" or "Return" button if visible + // This is iOS-specific logic - many keyboards have a "Done" button + await this.keyboardPress('return'); + debugPage('Dismissed keyboard using Return key'); + } catch (error) { + try { + // Method 2: Tap outside the keyboard area (top part of screen) + const { width, height } = await this.size(); + const tapX = width / 2; + const tapY = height / 4; // Tap in the upper quarter of the screen + + await this.tap({ left: tapX, top: tapY }); + debugPage('Dismissed keyboard by tapping outside'); + } catch (fallbackError) { + debugPage('Failed to dismiss keyboard:', fallbackError); + // Don't throw error - keyboard dismissal is optional + } + } + } + + async keyboardPress(key: string): Promise { + debugPage(`keyboard press: ${key}`); + + // Check if it's a combination key (contains '+') + if (key.includes('+')) { + // Handle hotkey combinations like 'cmd+1', 'cmd+tab', etc. + const keys = key.split('+').map((k) => k.trim().toLowerCase()); + + // Map common key names to PyAutoGUI format + const keyMapping: Record = { + cmd: 'command', + ctrl: 'ctrl', + alt: 'alt', + option: 'alt', + shift: 'shift', + tab: 'tab', + enter: 'enter', + return: 'enter', + space: 'space', + backspace: 'backspace', + delete: 'delete', + escape: 'escape', + esc: 'escape', + }; + + const mappedKeys = keys.map((k) => keyMapping[k] || k); + + await this.executePyAutoGUIAction({ + action: 'hotkey', + keys: mappedKeys, + }); + } else { + // Handle single key press + const keyMap: Record = { + Enter: 'enter', + Return: 'enter', + Tab: 'tab', + Space: 'space', + Backspace: 'backspace', + Delete: 'delete', + Escape: 'escape', + }; + + const mappedKey = keyMap[key] || key.toLowerCase(); + + await this.executePyAutoGUIAction({ + action: 'key', + key: mappedKey, + }); + } + } + + async scroll(scrollType: { + direction: 'up' | 'down' | 'left' | 'right'; + distance?: number; + }): Promise { + debugPage( + `scroll ${scrollType.direction}, distance: ${scrollType.distance || 'default'}`, + ); + + // Get current screen center for scroll + const { width, height } = await this.size(); + const centerX = width / 2; + const centerY = height / 2; + + const distance = scrollType.distance || 100; + + // Improved distance calculation to better match Android scroll behavior + // Android scroll distance is in pixels, we need to convert to effective scroll events + // Base the calculation on screen size for better proportional scrolling + const screenArea = width * height; + const scrollRatio = distance / Math.sqrt(screenArea); // Normalize by screen size + + // Calculate clicks with better scaling - aim for more responsive scrolling + let clicks: number; + if (distance <= 50) { + // Small scrolls: direct mapping for fine control + clicks = Math.max(3, Math.floor(distance / 8)); + } else if (distance <= 200) { + // Medium scrolls: moderate scaling + clicks = Math.max(8, Math.floor(distance / 12)); + } else { + // Large scrolls: aggressive scaling for significant movement + clicks = Math.max(15, Math.floor(distance / 10)); + } + + debugPage( + `Scroll distance: ${distance}px -> ${clicks} clicks (ratio: ${scrollRatio.toFixed(3)})`, + ); + + // Pass both distance and calculated clicks to Python server + const scrollAction: PyAutoGUIAction = { + action: 'scroll', + x: centerX, + y: centerY, + direction: scrollType.direction, + clicks: clicks, + distance: distance, // Pass original distance for server-side fine-tuning + scroll_type: 'trackpad', // Default to trackpad for smooth scrolling + }; + + // Always use mouse wheel/trackpad for scrolling (better compatibility) + if (this.options?.mirrorConfig) { + // iOS mirroring mode: use iOS coordinates directly + await this.executePyAutoGUIAction(scrollAction); + } else { + // Non-mirroring mode: adjust coordinates + const adjusted = this.adjustCoordinates(centerX, centerY); + await this.executePyAutoGUIAction({ + ...scrollAction, + x: adjusted.x, + y: adjusted.y, + scroll_type: 'wheel', // Use wheel for non-iOS devices + }); + } + } + + async getElementText(elementInfo: ElementInfo): Promise { + // For iOS/macOS, we can't easily extract text from elements + // This would require accessibility APIs or OCR + throw new Error('getElementText is not implemented for iOS devices'); + } + + // Required AndroidDevicePage interface methods + async getElementsNodeTree(): Promise { + // Simplified implementation, returns an empty node tree + return { + node: null, + children: [], + }; + } + + // @deprecated + async getElementsInfo(): Promise { + throw new Error('getElementsInfo is not implemented for iOS devices'); + } + + get mouse(): any { + return { + click: async (x: number, y: number, options: { button: string }) => { + // Directly use the provided coordinates, as these are already in the iOS coordinate system. + // The coordinate transformation from iOS to macOS will be handled inside executePyAutoGUIAction. + await this.executePyAutoGUIAction({ + action: 'click', + x: x, + y: y, + }); + }, + wheel: async (deltaX: number, deltaY: number) => { + throw new Error('mouse wheel is not implemented for iOS devices'); + }, + move: async (x: number, y: number) => { + await this.hover({ left: x, top: y }); + }, + drag: async ( + from: { x: number; y: number }, + to: { x: number; y: number }, + ) => { + // For iOS mirroring mode, pass coordinates directly; for non-mirroring mode, adjust using device pixel ratio + if (this.options?.mirrorConfig) { + await this.executePyAutoGUIAction({ + action: 'drag', + x: from.x, + y: from.y, + x2: to.x, + y2: to.y, + }); + } else { + const startAdjusted = this.adjustCoordinates(from.x, from.y); + const endAdjusted = this.adjustCoordinates(to.x, to.y); + + await this.executePyAutoGUIAction({ + action: 'drag', + x: startAdjusted.x, + y: startAdjusted.y, + x2: endAdjusted.x, + y2: endAdjusted.y, + }); + } + }, + }; + } + + get keyboard(): any { + return { + type: async (text: string, options?: AndroidDeviceInputOpt) => { + await this.input(text, options); + }, + press: async (action: any) => { + if (Array.isArray(action)) { + for (const a of action) { + await this.keyboardPress(a.key); + } + } else { + await this.keyboardPress(action.key); + } + }, + }; + } + + async clearInput(element: any): Promise { + // For iOS, we need to focus the input first by tapping it + if (element?.center) { + debugPage( + `Focusing input field at (${element.center[0]}, ${element.center[1]})`, + ); + await this.tap({ left: element.center[0], top: element.center[1] }); + await sleep(300); // Wait for focus and potential keyboard animation + } + + // Select all text and delete it - this works well on iOS + await this.keyboardPress('cmd+a'); + await sleep(100); + await this.keyboardPress('delete'); + await sleep(100); + + debugPage('Input field cleared'); + } + + url(): string { + return this.uri || ''; + } + + async scrollUntilTop(startingPoint?: Point): Promise { + const screenSize = await this.size(); + const point = startingPoint || { + left: screenSize.width / 2, + top: screenSize.height / 2, + }; + + // Scroll up multiple times to reach top + for (let i = 0; i < 10; i++) { + await this.scroll({ direction: 'up', distance: screenSize.height / 3 }); + await sleep(500); + } + } + + async scrollUntilBottom(startingPoint?: Point): Promise { + const screenSize = await this.size(); + const point = startingPoint || { + left: screenSize.width / 2, + top: screenSize.height / 2, + }; + + // Scroll down multiple times to reach bottom + for (let i = 0; i < 10; i++) { + await this.scroll({ direction: 'down', distance: screenSize.height / 3 }); + await sleep(500); + } + } + + async scrollUntilLeft(startingPoint?: Point): Promise { + const screenSize = await this.size(); + const point = startingPoint || { + left: screenSize.width / 2, + top: screenSize.height / 2, + }; + + // Scroll left multiple times to reach leftmost + for (let i = 0; i < 10; i++) { + await this.scroll({ direction: 'left', distance: screenSize.width / 3 }); + await sleep(500); + } + } + + async scrollUntilRight(startingPoint?: Point): Promise { + const screenSize = await this.size(); + const point = startingPoint || { + left: screenSize.width / 2, + top: screenSize.height / 2, + }; + + // Scroll right multiple times to reach rightmost + for (let i = 0; i < 10; i++) { + await this.scroll({ direction: 'right', distance: screenSize.width / 3 }); + await sleep(500); + } + } + + async scrollUp(distance?: number, startingPoint?: Point): Promise { + await this.scroll({ direction: 'up', distance }); + } + + async scrollDown(distance?: number, startingPoint?: Point): Promise { + await this.scroll({ direction: 'down', distance }); + } + + async scrollLeft(distance?: number, startingPoint?: Point): Promise { + await this.scroll({ direction: 'left', distance }); + } + + async scrollRight(distance?: number): Promise { + await this.scroll({ direction: 'right', distance }); + } + + async getXpathsById(id: string): Promise { + throw new Error('getXpathsById is not implemented for iOS devices'); + } + + async getXpathsByPoint( + point: Point, + isOrderSensitive: boolean, + ): Promise { + throw new Error('getXpathsByPoint is not implemented for iOS devices'); + } + + async getElementInfoByXpath(xpath: string): Promise { + throw new Error('getElementInfoByXpath is not implemented for iOS devices'); + } + + async back(): Promise { + // For iOS/macOS, we can simulate Command+[ or use system back gesture + await this.keyboardPress('cmd+['); + } + + async home(): Promise { + // For iOS simulator/mirroring, CMD+1 opens home screen + debugPage('Navigating to home screen using CMD+1'); + await this.keyboardPress('cmd+1'); + } + + async recentApps(): Promise { + // For iOS simulator/mirroring, CMD+2 opens app switcher + debugPage('Opening app switcher using CMD+2'); + await this.keyboardPress('cmd+2'); + } + + async longPress(x: number, y: number, duration?: number): Promise { + if (this.options?.mirrorConfig) { + await this.executePyAutoGUIAction({ + action: 'click', + x: x, + y: y, + }); + } else { + const adjustedPoint = this.adjustCoordinates(x, y); + await this.executePyAutoGUIAction({ + action: 'click', + x: adjustedPoint.x, + y: adjustedPoint.y, + }); + } + + // Simulate long press by holding for duration + if (duration) { + await sleep(duration); + } + } + + async pullDown( + startPoint?: Point, + distance?: number, + duration?: number, + ): Promise { + const screenSize = await this.size(); + const start = startPoint || { + left: screenSize.width / 2, + top: screenSize.height / 4, + }; + const end = { + left: start.left, + top: start.top + (distance || screenSize.height / 3), + }; + + if (this.options?.mirrorConfig) { + await this.executePyAutoGUIAction({ + action: 'drag', + x: start.left, + y: start.top, + x2: end.left, + y2: end.top, + }); + } else { + const startAdjusted = this.adjustCoordinates(start.left, start.top); + const endAdjusted = this.adjustCoordinates(end.left, end.top); + + await this.executePyAutoGUIAction({ + action: 'drag', + x: startAdjusted.x, + y: startAdjusted.y, + x2: endAdjusted.x, + y2: endAdjusted.y, + }); + } + } + + async pullUp( + startPoint?: Point, + distance?: number, + duration?: number, + ): Promise { + const screenSize = await this.size(); + const start = startPoint || { + left: screenSize.width / 2, + top: (screenSize.height * 3) / 4, + }; + const end = { + left: start.left, + top: start.top - (distance || screenSize.height / 3), + }; + + if (this.options?.mirrorConfig) { + await this.executePyAutoGUIAction({ + action: 'drag', + x: start.left, + y: start.top, + x2: end.left, + y2: end.top, + }); + } else { + const startAdjusted = this.adjustCoordinates(start.left, start.top); + const endAdjusted = this.adjustCoordinates(end.left, end.top); + + await this.executePyAutoGUIAction({ + action: 'drag', + x: startAdjusted.x, + y: startAdjusted.y, + x2: endAdjusted.x, + y2: endAdjusted.y, + }); + } + } + + async destroy(): Promise { + debugPage('destroy iOS device'); + this.destroyed = true; + + // Clean up server process if we started it + if (this.serverProcess) { + try { + debugPage('Terminating PyAutoGUI server process'); + this.serverProcess.kill('SIGTERM'); + this.serverProcess = undefined; + } catch (error) { + debugPage('Error terminating server process:', error); + } + } + } + + // Additional abstract methods from AbstractPage + async waitUntilNetworkIdle?(options?: { + idleTime?: number; + concurrency?: number; + }): Promise { + // Network idle detection is not applicable for iOS devices + await sleep(options?.idleTime || 1000); + } + + async evaluateJavaScript?(script: string): Promise { + throw new Error('evaluateJavaScript is not implemented for iOS devices'); + } +} diff --git a/packages/ios/src/utils/index.ts b/packages/ios/src/utils/index.ts new file mode 100644 index 000000000..52e8ab780 --- /dev/null +++ b/packages/ios/src/utils/index.ts @@ -0,0 +1,113 @@ +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; + +const execAsync = promisify(exec); + +export interface ScreenInfo { + width: number; + height: number; + dpr: number; +} + +/** + * Get macOS screen size information + */ +export async function getScreenSize(): Promise { + try { + // Use system_profiler to get display information + const { stdout } = await execAsync( + 'system_profiler SPDisplaysDataType -json', + ); + const data = JSON.parse(stdout); + + // Find the main display + const displays = data.SPDisplaysDataType?.[0]?.spdisplays_ndrvs || []; + const mainDisplay = + displays.find( + (display: any) => + display._name?.includes('Built-in') || + display._name?.includes('Display'), + ) || displays[0]; + + if (!mainDisplay) { + throw new Error('No display found'); + } + + // Parse resolution string like "2880 x 1800" + const resolution = + mainDisplay.spdisplays_resolution || mainDisplay._spdisplays_resolution; + const match = resolution?.match(/(\d+)\s*x\s*(\d+)/); + + if (!match) { + throw new Error(`Unable to parse screen resolution: ${resolution}`); + } + + const width = Number.parseInt(match[1], 10); + const height = Number.parseInt(match[2], 10); + + // Try to get pixel ratio from system info + const pixelDensity = + mainDisplay.spdisplays_pixel_density || mainDisplay.spdisplays_density; + let dpr = 1; + + if (pixelDensity?.includes('Retina')) { + dpr = 2; // Most Retina displays have 2x pixel ratio + } + + return { + width, + height, + dpr, + }; + } catch (error) { + // Fallback: try to get screen size using screencapture + try { + console.warn('Using fallback method to get screen size'); + // This is a fallback - assuming common screen sizes + return { + width: 1920, + height: 1080, + dpr: 2, + }; + } catch (fallbackError) { + throw new Error(`Failed to get screen size: ${(error as Error).message}`); + } + } +} + +/** + * Start the PyAutoGUI server + */ +export async function startPyAutoGUIServer(port = 1412): Promise { + const { spawn } = await import('node:child_process'); + const path = await import('node:path'); + + // Use __dirname in a way that works for both ESM and CommonJS + let currentDir: string; + if (typeof __dirname !== 'undefined') { + currentDir = __dirname; + } else { + const { fileURLToPath } = await import('node:url'); + currentDir = path.dirname(fileURLToPath(import.meta.url)); + } + + const serverPath = path.join(currentDir, '../../idb/auto_server.py'); + + const server = spawn('python3', [serverPath], { + stdio: 'inherit', + env: { + ...process.env, + PYTHONUNBUFFERED: '1', + }, + }); + + server.on('error', (error) => { + console.error('Failed to start PyAutoGUI server:', error); + throw error; + }); + + // Wait a bit for server to start + await new Promise((resolve) => setTimeout(resolve, 2000)); + + console.log(`PyAutoGUI server started on port ${port}`); +} diff --git a/packages/ios/tests/unit-test/agent.test.ts b/packages/ios/tests/unit-test/agent.test.ts new file mode 100644 index 000000000..292a3d3c8 --- /dev/null +++ b/packages/ios/tests/unit-test/agent.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { iOSAgent } from '../../src/agent'; +import type { iOSDevice } from '../../src/page'; + +describe('iOS Agent', () => { + describe('constructor', () => { + it('should create an iOS agent instance', () => { + // Create a mock iOS device + const mockDevice = {} as iOSDevice; + + const agent = new iOSAgent(mockDevice); + + expect(agent).toBeDefined(); + expect(agent.page).toBe(mockDevice); + }); + }); +}); diff --git a/packages/ios/tests/unit-test/utils.test.ts b/packages/ios/tests/unit-test/utils.test.ts new file mode 100644 index 000000000..dadb95529 --- /dev/null +++ b/packages/ios/tests/unit-test/utils.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import type { ScreenInfo } from '../../src/utils'; + +describe('iOS Utils', () => { + describe('ScreenInfo interface', () => { + it('should have correct type definition', () => { + const screenInfo: ScreenInfo = { + width: 1920, + height: 1080, + dpr: 2, + }; + + expect(screenInfo.width).toBe(1920); + expect(screenInfo.height).toBe(1080); + expect(screenInfo.dpr).toBe(2); + }); + }); +}); diff --git a/packages/ios/tsconfig.json b/packages/ios/tsconfig.json new file mode 100644 index 000000000..c4ca89089 --- /dev/null +++ b/packages/ios/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": ".", + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "preserve", + "lib": ["DOM", "ESNext"], + "moduleResolution": "bundler", + "resolveJsonModule": true, + "rootDir": "src", + "skipLibCheck": true, + "strict": true, + "module": "ES2020", + "target": "es2020", + "types": ["node"] + }, + "exclude": ["**/node_modules"], + "include": ["src"] +} diff --git a/packages/ios/vitest.config.ts b/packages/ios/vitest.config.ts new file mode 100644 index 000000000..4ac6027d5 --- /dev/null +++ b/packages/ios/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +}); diff --git a/packages/web-integration/src/yaml/utils.ts b/packages/web-integration/src/yaml/utils.ts index a9d727a7e..ffadc41e7 100644 --- a/packages/web-integration/src/yaml/utils.ts +++ b/packages/web-integration/src/yaml/utils.ts @@ -42,6 +42,10 @@ export function parseYamlScript( typeof obj.android !== 'undefined' ? Object.assign({}, obj.android || {}) : undefined; + const ios = + typeof obj.ios !== 'undefined' + ? Object.assign({}, obj.ios || {}) + : undefined; const webConfig = obj.web || obj.target; // no need to handle null case, because web has required parameters url const web = typeof webConfig !== 'undefined' @@ -49,23 +53,26 @@ export function parseYamlScript( : undefined; if (!ignoreCheckingTarget) { - // make sure at least one of target/web/android is provided + // make sure at least one of target/web/android/ios is provided assert( - web || android, - `at least one of "target", "web", or "android" properties is required in yaml script${pathTip}`, + web || android || ios, + `at least one of "target", "web", "android", or "ios" properties is required in yaml script${pathTip}`, ); - // make sure only one of target/web/android is provided + // make sure only one of target/web/android/ios is provided + const configCount = [web, android, ios].filter(Boolean).length; assert( - (web && !android) || (!web && android), - `only one of "target", "web", or "android" properties is allowed in yaml script${pathTip}`, + configCount === 1, + `only one of "target", "web", "android", or "ios" properties is allowed in yaml script${pathTip}`, ); // make sure the config is valid - if (web || android) { + if (web || android || ios) { assert( - typeof web === 'object' || typeof android === 'object', - `property "target/web/android" must be an object${pathTip}`, + typeof web === 'object' || + typeof android === 'object' || + typeof ios === 'object', + `property "target/web/android/ios" must be an object${pathTip}`, ); } } diff --git a/packages/web-integration/tests/unit-test/yaml/utils.test.ts b/packages/web-integration/tests/unit-test/yaml/utils.test.ts index 34cf2071f..fa203b680 100644 --- a/packages/web-integration/tests/unit-test/yaml/utils.test.ts +++ b/packages/web-integration/tests/unit-test/yaml/utils.test.ts @@ -65,6 +65,35 @@ tasks: expect(result.android?.deviceId).toBe('001234567890'); }); + test('ios configuration', () => { + const yamlContent = ` +ios: + serverPort: 1412 + serverUrl: "http://localhost:1412" + autoDismissKeyboard: true + mirrorConfig: + mirrorX: 100 + mirrorY: 200 + mirrorWidth: 400 + mirrorHeight: 800 + launch: "https://www.apple.com" + output: "./results.json" +tasks: +- sleep: 1000 +`; + + const result = parseYamlScript(yamlContent); + expect(result.ios?.serverPort).toBe(1412); + expect(result.ios?.serverUrl).toBe('http://localhost:1412'); + expect(result.ios?.autoDismissKeyboard).toBe(true); + expect(result.ios?.mirrorConfig?.mirrorX).toBe(100); + expect(result.ios?.mirrorConfig?.mirrorY).toBe(200); + expect(result.ios?.mirrorConfig?.mirrorWidth).toBe(400); + expect(result.ios?.mirrorConfig?.mirrorHeight).toBe(800); + expect(result.ios?.launch).toBe('https://www.apple.com'); + expect(result.ios?.output).toBe('./results.json'); + }); + test('illegal android deviceId', () => { const yamlContent = ` android: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3baffb40..642fe8074 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -245,6 +245,70 @@ importers: specifier: ^5.8.3 version: 5.8.3 + apps/ios-playground: + dependencies: + '@ant-design/icons': + specifier: ^5.3.1 + version: 5.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@midscene/core': + specifier: workspace:* + version: link:../../packages/core + '@midscene/ios': + specifier: workspace:* + version: link:../../packages/ios + '@midscene/shared': + specifier: workspace:* + version: link:../../packages/shared + '@midscene/visualizer': + specifier: workspace:* + version: link:../../packages/visualizer + '@midscene/web': + specifier: workspace:* + version: link:../../packages/web-integration + antd: + specifier: ^5.21.6 + version: 5.21.6(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + dayjs: + specifier: ^1.11.11 + version: 1.11.13 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@rsbuild/core': + specifier: ^1.3.22 + version: 1.4.15 + '@rsbuild/plugin-less': + specifier: ^1.2.4 + version: 1.2.4(@rsbuild/core@1.4.15) + '@rsbuild/plugin-node-polyfill': + specifier: 1.3.0 + version: 1.3.0(@rsbuild/core@1.4.15) + '@rsbuild/plugin-react': + specifier: ^1.3.1 + version: 1.3.5(@rsbuild/core@1.4.15) + '@rsbuild/plugin-svgr': + specifier: ^1.1.1 + version: 1.2.0(@rsbuild/core@1.4.15)(typescript@5.8.3) + '@types/react': + specifier: ^18.3.1 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.23) + archiver: + specifier: ^6.0.0 + version: 6.0.2 + less: + specifier: ^4.2.0 + version: 4.3.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + apps/recorder-form: dependencies: '@midscene/recorder': @@ -486,6 +550,9 @@ importers: '@midscene/core': specifier: workspace:* version: link:../core + '@midscene/ios': + specifier: workspace:* + version: link:../ios '@midscene/shared': specifier: workspace:* version: link:../shared @@ -649,6 +716,77 @@ importers: specifier: 3.0.5 version: 3.0.5(@types/debug@4.1.12)(@types/node@22.15.3)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1) + packages/ios: + dependencies: + '@midscene/core': + specifier: workspace:* + version: link:../core + '@midscene/shared': + specifier: workspace:* + version: link:../shared + '@midscene/web': + specifier: workspace:* + version: link:../web-integration + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + devDependencies: + '@modern-js/module-tools': + specifier: ^2.60.3 + version: 2.60.6(typescript@5.8.3) + '@types/node': + specifier: ^22.10.5 + version: 22.15.3 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + tsx: + specifier: ^4.17.0 + version: 4.19.2 + typescript: + specifier: ^5.7.2 + version: 5.8.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.15.3)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1) + + packages/ios-playground: + dependencies: + '@midscene/ios': + specifier: workspace:* + version: link:../ios + '@midscene/shared': + specifier: workspace:* + version: link:../shared + '@midscene/web': + specifier: workspace:* + version: link:../web-integration + cors: + specifier: 2.8.5 + version: 2.8.5 + express: + specifier: ^4.21.2 + version: 4.21.2 + open: + specifier: 10.1.0 + version: 10.1.0 + devDependencies: + '@modern-js/module-tools': + specifier: 2.60.6 + version: 2.60.6(typescript@5.8.3) + '@types/cors': + specifier: 2.8.12 + version: 2.8.12 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/node': + specifier: ^18.0.0 + version: 18.19.62 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + packages/mcp: dependencies: sharp: @@ -1039,18 +1177,36 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@ast-grep/napi-darwin-arm64@0.16.0': + resolution: {integrity: sha512-ESjIg03S0ln+8CP43TKqY6+QPL2Kkm+6iMS5kAUMVtH/WNWd2z0oQLg9bmadUNPylYbB42B3zRtuTKwm/nCpdA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@ast-grep/napi-darwin-arm64@0.37.0': resolution: {integrity: sha512-QAiIiaAbLvMEg/yBbyKn+p1gX2/FuaC0SMf7D7capm/oG4xGMzdeaQIcSosF4TCxxV+hIH4Bz9e4/u7w6Bnk3Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@ast-grep/napi-darwin-x64@0.16.0': + resolution: {integrity: sha512-a7cOdfACgmsGyTSMLkVuGiK/v+M8eTgUWew5X/4gcPHX4GcqVbptP82kbtiVVWZW5QXX2j6VYkFCsmJ7knkXBA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@ast-grep/napi-darwin-x64@0.37.0': resolution: {integrity: sha512-zvcvdgekd4ySV3zUbUp8HF5nk5zqwiMXTuVzTUdl/w08O7JjM6XPOIVT+d2o/MqwM9rsXdzdergY5oY2RdhSPA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@ast-grep/napi-linux-arm64-gnu@0.16.0': + resolution: {integrity: sha512-5BaueDB3ZJxLy/qGDzWO16zSmU02da96ABkp6S210OTlaThDgLpjfztoI10iwu/f3WpTnOvbggjfzOLWUAL3Aw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@ast-grep/napi-linux-arm64-gnu@0.37.0': resolution: {integrity: sha512-L7Sj0lXy8X+BqSMgr1LB8cCoWk0rericdeu+dC8/c8zpsav5Oo2IQKY1PmiZ7H8IHoFBbURLf8iklY9wsD+cyA==} engines: {node: '>= 10'} @@ -1063,6 +1219,12 @@ packages: cpu: [arm64] os: [linux] + '@ast-grep/napi-linux-x64-gnu@0.16.0': + resolution: {integrity: sha512-QjiY45TvPI50I2UxPlfPuoeDeEYJxGDyLegqYfrLsxtdv+wX2Jdgjew6myiMXCVG9oJWgtmp/z28zpl7H8YLPA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@ast-grep/napi-linux-x64-gnu@0.37.0': resolution: {integrity: sha512-TViz5/klqre6aSmJzswEIjApnGjJzstG/SE8VDWsrftMBMYt2PTu3MeluZVwzSqDao8doT/P+6U11dU05UOgxw==} engines: {node: '>= 10'} @@ -1075,24 +1237,46 @@ packages: cpu: [x64] os: [linux] + '@ast-grep/napi-win32-arm64-msvc@0.16.0': + resolution: {integrity: sha512-4OCpEf44h63RVFiNA2InIoRNlTB2XJUq1nUiFacTagSP5L3HwnZQ4URC1+fdmZh1ESedm7KxzvhgByqGeUyzgA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@ast-grep/napi-win32-arm64-msvc@0.37.0': resolution: {integrity: sha512-TjQA4cFoIEW2bgjLkaL9yqT4XWuuLa5MCNd0VCDhGRDMNQ9+rhwi9eLOWRaap3xzT7g+nlbcEHL3AkVCD2+b3A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] + '@ast-grep/napi-win32-ia32-msvc@0.16.0': + resolution: {integrity: sha512-bJW9w9btdE9OuGKZSNiKkBR+Ax4113VhiJgxC2t9KbhmOsOM9E4l2U570h+DrjWdf+H3Oyb4Cz8so2noh5LQqw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + '@ast-grep/napi-win32-ia32-msvc@0.37.0': resolution: {integrity: sha512-uNmVka8fJCdYsyOlF9aZqQMLTatEYBynjChVTzUfFMDfmZ0bihs/YTqJVbkSm8TZM7CUX82apvn50z/dX5iWRA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] + '@ast-grep/napi-win32-x64-msvc@0.16.0': + resolution: {integrity: sha512-+qUauPADrUIBgSGMmjnCBuy2xuGlG97qjrRAYo9y+Mv9gGnAMpGA5zzLZArHcQwNzXwFB9aIqavtCL+tu28wHg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@ast-grep/napi-win32-x64-msvc@0.37.0': resolution: {integrity: sha512-vCiFOT3hSCQuHHfZ933GAwnPzmL0G04JxQEsBRfqONywyT8bSdDc/ECpAfr3S9VcS4JZ9/F6tkePKW/Om2Dq2g==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@ast-grep/napi@0.16.0': + resolution: {integrity: sha512-qOqQG9o97Q4tIZXZyWI7JuDZGJi3yibTN7LiGLmnzNLaIhmpv26BWj5OYJibUyQLVH/aTjdZSNx4spa7EihUzg==} + engines: {node: '>= 10'} + '@ast-grep/napi@0.37.0': resolution: {integrity: sha512-Hb4o6h1Pf6yRUAX07DR4JVY7dmQw+RVQMW5/m55GoiAT/VRoKCWBtIUPPOnqDVhbx1Cjfil9b6EDrgJsUAujEQ==} engines: {node: '>= 10'} @@ -1278,58 +1462,110 @@ packages: '@changesets/apply-release-plan@6.1.4': resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} + '@changesets/apply-release-plan@7.0.12': + resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} + '@changesets/assemble-release-plan@5.2.4': resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==} + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + '@changesets/changelog-git@0.1.14': resolution: {integrity: sha512-+vRfnKtXVWsDDxGctOfzJsPhaCdXRYoe+KyWYoq5X/GqoISREiat0l3L8B0a453B2B4dfHGcZaGyowHbp9BSaA==} + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + '@changesets/cli@2.24.1': resolution: {integrity: sha512-7Lz1inqGQjBrXgnXlENtzQ7EmO/9c+09d9oi8XoK4ARqlJe8GpafjqKRobcjcA/TTI7Fn2+cke4CrXFZfVF8Rw==} hasBin: true + '@changesets/cli@2.29.5': + resolution: {integrity: sha512-0j0cPq3fgxt2dPdFsg4XvO+6L66RC0pZybT9F4dG5TBrLA3jA/1pNkdTXH9IBBVHkgsKrNKenI3n1mPyPlIydg==} + hasBin: true + '@changesets/config@2.3.1': resolution: {integrity: sha512-PQXaJl82CfIXddUOppj4zWu+987GCw2M+eQcOepxN5s+kvnsZOwjEJO3DH9eVy+OP6Pg/KFEWdsECFEYTtbg6w==} + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + '@changesets/errors@0.1.4': resolution: {integrity: sha512-HAcqPF7snsUJ/QzkWoKfRfXushHTu+K5KZLJWPb34s4eCZShIf8BFO3fwq6KU8+G7L5KdtN2BzQAXOSXEyiY9Q==} + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + '@changesets/get-dependents-graph@1.3.6': resolution: {integrity: sha512-Q/sLgBANmkvUm09GgRsAvEtY3p1/5OCzgBE5vX3vgb5CvW0j7CEljocx5oPXeQSNph6FXulJlXV3Re/v3K3P3Q==} + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + '@changesets/get-release-plan@3.0.17': resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==} + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + '@changesets/get-version-range-type@0.3.2': resolution: {integrity: sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg==} + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + '@changesets/git@1.5.0': resolution: {integrity: sha512-Xo8AT2G7rQJSwV87c8PwMm6BAc98BnufRMsML7m7Iw8Or18WFvFmxqG5aOL5PBvhgq9KrKvaeIBNIymracSuHg==} '@changesets/git@2.0.0': resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + '@changesets/logger@0.0.5': resolution: {integrity: sha512-gJyZHomu8nASHpaANzc6bkQMO9gU/ib20lqew1rVx753FOxffnCrJlGIeQVxNWCqM+o6OOleCo/ivL8UAO5iFw==} + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + '@changesets/parse@0.3.16': resolution: {integrity: sha512-127JKNd167ayAuBjUggZBkmDS5fIKsthnr9jr6bdnuUljroiERW7FBTDNnNVyJ4l69PzR57pk6mXQdtJyBCJKg==} + '@changesets/parse@0.4.1': + resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + '@changesets/pre@1.0.14': resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + '@changesets/read@0.5.9': resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + '@changesets/types@4.1.0': resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} '@changesets/types@5.2.1': resolution: {integrity: sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==} + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + '@changesets/write@0.1.9': resolution: {integrity: sha512-E90ZrsrfJVOOQaP3Mm5Xd7uDwBAqq3z5paVEavTHKA8wxi7NAL8CmjgbGxSFuiP7ubnJA2BuHlrdE4z86voGOg==} + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -1479,6 +1715,18 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.17.19': + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.2': + resolution: {integrity: sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -1491,6 +1739,18 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm@0.17.19': + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.2': + resolution: {integrity: sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -1503,6 +1763,18 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-x64@0.17.19': + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.2': + resolution: {integrity: sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -1515,6 +1787,18 @@ packages: cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.17.19': + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.2': + resolution: {integrity: sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -1527,6 +1811,18 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.17.19': + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.2': + resolution: {integrity: sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -1539,6 +1835,18 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.17.19': + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.2': + resolution: {integrity: sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -1551,6 +1859,18 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.17.19': + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.2': + resolution: {integrity: sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -1563,6 +1883,18 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.17.19': + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.2': + resolution: {integrity: sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -1575,6 +1907,18 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.17.19': + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.2': + resolution: {integrity: sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -1587,6 +1931,18 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.17.19': + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.2': + resolution: {integrity: sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -1599,6 +1955,18 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.17.19': + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.2': + resolution: {integrity: sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -1611,6 +1979,18 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.17.19': + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.2': + resolution: {integrity: sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -1623,6 +2003,18 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.17.19': + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.2': + resolution: {integrity: sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -1635,6 +2027,18 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.17.19': + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.2': + resolution: {integrity: sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -1647,6 +2051,18 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.17.19': + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.2': + resolution: {integrity: sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -1659,6 +2075,18 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.17.19': + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.2': + resolution: {integrity: sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -1671,6 +2099,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/netbsd-x64@0.17.19': + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.2': + resolution: {integrity: sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -1689,6 +2129,18 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.17.19': + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.2': + resolution: {integrity: sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -1701,6 +2153,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/sunos-x64@0.17.19': + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.2': + resolution: {integrity: sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -1713,6 +2177,18 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.17.19': + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.2': + resolution: {integrity: sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -1725,6 +2201,18 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.17.19': + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.2': + resolution: {integrity: sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -1737,6 +2225,18 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.17.19': + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.2': + resolution: {integrity: sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -2387,6 +2887,94 @@ packages: resolution: {integrity: sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==} engines: {node: '>=18'} + '@modern-js/core@2.60.6': + resolution: {integrity: sha512-zfhOkB8uoOH6Mj06E9/sD8k+efC2b+cG1X/fDzRGyMI91k25Pqpq0yGl5lWn0Zz0/WnH2bLkHlIRlAcOmGgI8g==} + + '@modern-js/module-tools@2.60.6': + resolution: {integrity: sha512-zixrEPOfsLn3NtcUEdQf4Km5O4id/PkFbhrOKubBebXowMQx8RbWRQPvy+2qOy3x8Bg/n+U+yhrMqKW5GmWB/Q==} + engines: {node: '>=16.0.0'} + hasBin: true + peerDependencies: + typescript: ^4 || ^5 + peerDependenciesMeta: + typescript: + optional: true + + '@modern-js/node-bundle-require@2.60.6': + resolution: {integrity: sha512-xrchg6yAg9dNPB9aAd94/ftpcIG21LXD//0EVxpdcFsMaHYbtXKG8hcA/9MgxlEA1ELJwxedRQov4N3/wFfvNQ==} + + '@modern-js/plugin-changeset@2.60.6': + resolution: {integrity: sha512-cQyFVxoWibC/Am2fw18eEpZPRaIzT6sNu7UXd0J7/E6xdlUVEVndZiOfpCEYSgo8EgTHG4JVxPDWsIJwvbfrrQ==} + + '@modern-js/plugin-i18n@2.60.6': + resolution: {integrity: sha512-0yVbRH/bkXEs7oh75GcPaK9CqV+j+UPNVGOIvMliH3ZmHnesGDbK9huzYscBfoDb9zNLmArI9lNsDmS/074mAQ==} + + '@modern-js/plugin@2.60.6': + resolution: {integrity: sha512-27CPUvnKEerq3kY7uPTYLO9PvP6LB2fV3Xo1RDPTNFgx3WhDUgiPtbqDi2yfzMIgPsaPn9AEdoJMGtD9EuXqfA==} + + '@modern-js/swc-plugins-darwin-arm64@0.6.11': + resolution: {integrity: sha512-UMH0bo20vcD10//F7KaINLfuHawQBVcWCCyJvkYOiBt7e1tUjeybKu+y6eNq1USyFVElEMul8ytnYdwAS9sY+w==} + engines: {node: '>=14.12'} + cpu: [arm64] + os: [darwin] + + '@modern-js/swc-plugins-darwin-x64@0.6.11': + resolution: {integrity: sha512-qLcXAnM/IGcZX7B0MvxSdZjvgGofhOtHaEdj8CFkt75CzriBMu7lrGsRP4+paXbFAgM4vp7ZV7julaFrrDCoZw==} + engines: {node: '>=14.12'} + cpu: [x64] + os: [darwin] + + '@modern-js/swc-plugins-linux-arm64-gnu@0.6.11': + resolution: {integrity: sha512-3WcTpQqJp7RM/i8lLe+GjOCx17ljKdPbxlIY4LkJe+SQXATd3YducTtNqsEAdBA8Au907rI1ImuhN0kxvR97jQ==} + engines: {node: '>=14.12'} + cpu: [arm64] + os: [linux] + + '@modern-js/swc-plugins-linux-arm64-musl@0.6.11': + resolution: {integrity: sha512-RFE3xJWbABM8ZzPMVqlr3qovgnwamgpGjGN15rJ4tVqieO2FORQ14xSQE1ROBun6kIADUD+TxnepTlGb7EJg0w==} + engines: {node: '>=14.12'} + cpu: [arm64] + os: [linux] + + '@modern-js/swc-plugins-linux-x64-gnu@0.6.11': + resolution: {integrity: sha512-vSDF5aznEtnS0kHFm7UXHHaFzEZEyTV+CTQOVEp84hU5+HW1fMR+MFmbeCJnqXpB+R8Kg93SL4KWePGzm2ZNWw==} + engines: {node: '>=14.12'} + cpu: [x64] + os: [linux] + + '@modern-js/swc-plugins-linux-x64-musl@0.6.11': + resolution: {integrity: sha512-jfwQeuSmHbgvw9fjFRi5ZkLWejF8WZkYew0MGHqkSyLYZU+p7RgGo+tSpT4CK+b6p9qzh8LxoFpYjx6YycCY/w==} + engines: {node: '>=14.12'} + cpu: [x64] + os: [linux] + + '@modern-js/swc-plugins-win32-arm64-msvc@0.6.11': + resolution: {integrity: sha512-MWbuMTdGZ81Xce+OmnM3xsKKQIHmp9Eq0FlueRIPQdSLCO6IogIjALsMum4Vd1tRnEosKXj0xcuD4IsAWgtW6w==} + engines: {node: '>=14.12'} + cpu: [arm64] + os: [win32] + + '@modern-js/swc-plugins-win32-x64-msvc@0.6.11': + resolution: {integrity: sha512-UH4BeAjfs7Z6sZQOaFjgOB9h99qAPRtRFbmkQ+77FDlwNdjii0xPJvdNPCxAJITnqHjHSqrbz9rFDSrWnI9eKg==} + engines: {node: '>=14.12'} + cpu: [x64] + os: [win32] + + '@modern-js/swc-plugins@0.6.11': + resolution: {integrity: sha512-bXwjeFa5mg1hD6zzSHzw94FMNFb9SV458g76zWYfXRw7wMjp97NYl6n3dOTOixdngC0JqZctSbP4mJwSZ0p3Gw==} + engines: {node: '>=14.17.6'} + peerDependencies: + '@swc/helpers': '>=0.5.3' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@modern-js/types@2.60.6': + resolution: {integrity: sha512-Tjh03D6lW34BmbKm5CV7SgtjSnOIjFQhRh+pExCMpSQUgJOWSooboEVsZQ2f8zdyxijI1MSSGEIt4ak30Vsvng==} + + '@modern-js/utils@2.60.6': + resolution: {integrity: sha512-rAeqAHiUUnStwBTkP1tdQSz29o/Qtoc2OUfz6TEAtEPoAxcFSc44+hwux7mQkSxXSzBjkbev5RMkwVwuM2FWtw==} + '@module-federation/error-codes@0.14.0': resolution: {integrity: sha512-GGk+EoeSACJikZZyShnLshtq9E2eCrDWbRiB4QAFXCX4oYmGgFfzXlx59vMNwqTKPJWxkEGnPYacJMcr2YYjag==} @@ -2976,6 +3564,10 @@ packages: resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} engines: {node: '>=14.0.0'} + '@rollup/pluginutils@4.1.1': + resolution: {integrity: sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==} + engines: {node: '>= 8.0.0'} + '@rollup/rollup-android-arm-eabi@4.24.3': resolution: {integrity: sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==} cpu: [arm] @@ -3534,6 +4126,9 @@ packages: peerDependencies: '@svgr/core': '*' + '@swc/helpers@0.5.13': + resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -3785,9 +4380,6 @@ packages: '@types/node@16.9.1': resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - '@types/node@18.19.118': - resolution: {integrity: sha512-hIPK0hSrrcaoAu/gJMzN3QClXE4QdCdFvaenJ0JsjIbExP1JFFVH+RHcBt25c9n8bx5dkIfqKE+uw6BmBns7ug==} - '@types/node@18.19.62': resolution: {integrity: sha512-UOGhw+yZV/icyM0qohQVh3ktpY40Sp7tdTW7HxG3pTd7AiMrlFlAJNUrGK9t5mdW0+ViQcFV74zCSIx9ZJpncA==} @@ -3881,9 +4473,23 @@ packages: peerDependencies: react: '>=18.3.1' + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.0.5': resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.0.5': resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} peerDependencies: @@ -3895,21 +4501,36 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.0.5': resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} '@vitest/pretty-format@3.1.1': resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.0.5': resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.0.5': resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.0.5': resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.0.5': resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} @@ -4140,6 +4761,9 @@ packages: any-base@1.1.0: resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -4683,6 +5307,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -4762,6 +5390,9 @@ packages: engines: {node: '>=16'} hasBin: true + convert-source-map@1.8.0: + resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4887,6 +5518,11 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -4919,6 +5555,10 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -5273,6 +5913,16 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.2: + resolution: {integrity: sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -5346,6 +5996,9 @@ packages: estree-util-visit@2.0.0: resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -5478,6 +6131,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -5588,6 +6245,10 @@ packages: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -5653,6 +6314,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generic-names@4.0.0: + resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -5731,6 +6395,10 @@ packages: resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} deprecated: Glob versions prior to v9 are no longer supported + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -5948,6 +6616,10 @@ packages: human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} + human-id@4.1.1: + resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} + hasBin: true + human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -5976,6 +6648,15 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + icss-replace-symbols@1.1.0: + resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6721,6 +7402,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -7110,6 +7794,9 @@ packages: resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==} engines: {node: '>=0.8.0'} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nano-staged@0.8.0: resolution: {integrity: sha512-QSEqPGTCJbkHU2yLvfY6huqYPjdBrOaTMKatO1F8nCSrkQGXeKwtCiCnsdxnuMhbg3DTVywKaeWLGCE5oJpq0g==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7161,6 +7848,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} @@ -7381,6 +8072,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -7491,6 +8185,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -7531,6 +8228,10 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + pixelmatch@4.0.2: resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} hasBin: true @@ -7593,6 +8294,42 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules@4.3.1: + resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} + peerDependencies: + postcss: ^8.0.0 + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.1: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} @@ -7702,6 +8439,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + query-string@9.1.1: resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==} engines: {node: '>=18'} @@ -8310,6 +9050,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-identifier@0.4.2: + resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -8721,6 +9464,9 @@ packages: spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -8794,6 +9540,9 @@ packages: string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + string-hash@1.1.3: + resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -8866,6 +9615,9 @@ packages: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} + style-inject@0.3.0: + resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} + style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -8875,6 +9627,11 @@ packages: stylis@4.3.4: resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} + sucrase@3.29.0: + resolution: {integrity: sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A==} + engines: {node: '>=8'} + hasBin: true + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -8983,6 +9740,13 @@ packages: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@1.21.0: resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} engines: {node: '>=10.18'} @@ -9030,6 +9794,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -9120,6 +9888,9 @@ packages: '@rspack/core': optional: true + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -9134,6 +9905,10 @@ packages: '@swc/wasm': optional: true + tsconfig-paths-webpack-plugin@4.1.0: + resolution: {integrity: sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==} + engines: {node: '>=10.13.0'} + tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -9381,6 +10156,11 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.0.5: resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -9417,6 +10197,31 @@ packages: terser: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.0.5: resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -9468,6 +10273,10 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -9920,33 +10729,64 @@ snapshots: lru-cache: 10.4.3 optional: true + '@ast-grep/napi-darwin-arm64@0.16.0': + optional: true + '@ast-grep/napi-darwin-arm64@0.37.0': optional: true + '@ast-grep/napi-darwin-x64@0.16.0': + optional: true + '@ast-grep/napi-darwin-x64@0.37.0': optional: true + '@ast-grep/napi-linux-arm64-gnu@0.16.0': + optional: true + '@ast-grep/napi-linux-arm64-gnu@0.37.0': optional: true '@ast-grep/napi-linux-arm64-musl@0.37.0': optional: true + '@ast-grep/napi-linux-x64-gnu@0.16.0': + optional: true + '@ast-grep/napi-linux-x64-gnu@0.37.0': optional: true '@ast-grep/napi-linux-x64-musl@0.37.0': optional: true + '@ast-grep/napi-win32-arm64-msvc@0.16.0': + optional: true + '@ast-grep/napi-win32-arm64-msvc@0.37.0': optional: true + '@ast-grep/napi-win32-ia32-msvc@0.16.0': + optional: true + '@ast-grep/napi-win32-ia32-msvc@0.37.0': optional: true + '@ast-grep/napi-win32-x64-msvc@0.16.0': + optional: true + '@ast-grep/napi-win32-x64-msvc@0.37.0': optional: true + '@ast-grep/napi@0.16.0': + optionalDependencies: + '@ast-grep/napi-darwin-arm64': 0.16.0 + '@ast-grep/napi-darwin-x64': 0.16.0 + '@ast-grep/napi-linux-arm64-gnu': 0.16.0 + '@ast-grep/napi-linux-x64-gnu': 0.16.0 + '@ast-grep/napi-win32-arm64-msvc': 0.16.0 + '@ast-grep/napi-win32-ia32-msvc': 0.16.0 + '@ast-grep/napi-win32-x64-msvc': 0.16.0 + '@ast-grep/napi@0.37.0': optionalDependencies: '@ast-grep/napi-darwin-arm64': 0.37.0 @@ -10204,6 +11044,22 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.1 + '@changesets/apply-release-plan@7.0.12': + dependencies: + '@changesets/config': 3.1.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.2 + '@changesets/assemble-release-plan@5.2.4': dependencies: '@babel/runtime': 7.27.0 @@ -10213,10 +11069,23 @@ snapshots: '@manypkg/get-packages': 1.1.3 semver: 7.7.1 + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.2 + '@changesets/changelog-git@0.1.14': dependencies: '@changesets/types': 5.2.1 + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + '@changesets/cli@2.24.1': dependencies: '@babel/runtime': 7.27.0 @@ -10253,6 +11122,37 @@ snapshots: term-size: 2.2.1 tty-table: 4.2.3 + '@changesets/cli@2.29.5': + dependencies: + '@changesets/apply-release-plan': 7.0.12 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.13 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + external-editor: 3.1.0 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.2 + spawndamnit: 3.0.1 + term-size: 2.2.1 + '@changesets/config@2.3.1': dependencies: '@changesets/errors': 0.1.4 @@ -10263,10 +11163,24 @@ snapshots: fs-extra: 7.0.1 micromatch: 4.0.8 + '@changesets/config@3.1.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + '@changesets/errors@0.1.4': dependencies: extendable-error: 0.1.7 + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + '@changesets/get-dependents-graph@1.3.6': dependencies: '@changesets/types': 5.2.1 @@ -10275,6 +11189,13 @@ snapshots: fs-extra: 7.0.1 semver: 7.7.1 + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.2 + '@changesets/get-release-plan@3.0.17': dependencies: '@babel/runtime': 7.27.0 @@ -10285,8 +11206,19 @@ snapshots: '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 + '@changesets/get-release-plan@4.0.13': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + '@changesets/get-version-range-type@0.3.2': {} + '@changesets/get-version-range-type@0.4.0': {} + '@changesets/git@1.5.0': dependencies: '@babel/runtime': 7.27.0 @@ -10306,15 +11238,32 @@ snapshots: micromatch: 4.0.8 spawndamnit: 2.0.0 + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + '@changesets/logger@0.0.5': dependencies: chalk: 2.4.2 + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + '@changesets/parse@0.3.16': dependencies: '@changesets/types': 5.2.1 js-yaml: 3.14.1 + '@changesets/parse@0.4.1': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 3.14.1 + '@changesets/pre@1.0.14': dependencies: '@babel/runtime': 7.27.0 @@ -10323,6 +11272,13 @@ snapshots: '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + '@changesets/read@0.5.9': dependencies: '@babel/runtime': 7.27.0 @@ -10334,10 +11290,27 @@ snapshots: fs-extra: 7.0.1 p-filter: 2.1.0 + '@changesets/read@0.6.5': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.1 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + '@changesets/types@4.1.0': {} '@changesets/types@5.2.1': {} + '@changesets/types@6.1.0': {} + '@changesets/write@0.1.9': dependencies: '@babel/runtime': 7.27.0 @@ -10346,6 +11319,13 @@ snapshots: human-id: 1.0.2 prettier: 1.19.1 + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.1 + prettier: 2.8.8 + '@colors/colors@1.6.0': {} '@commitlint/cli@19.8.0(@types/node@22.15.3)(typescript@5.8.3)': @@ -10530,100 +11510,202 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true + '@esbuild/android-arm64@0.17.19': + optional: true + + '@esbuild/android-arm64@0.19.2': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.23.1': optional: true + '@esbuild/android-arm@0.17.19': + optional: true + + '@esbuild/android-arm@0.19.2': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.23.1': optional: true + '@esbuild/android-x64@0.17.19': + optional: true + + '@esbuild/android-x64@0.19.2': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.23.1': optional: true + '@esbuild/darwin-arm64@0.17.19': + optional: true + + '@esbuild/darwin-arm64@0.19.2': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true + '@esbuild/darwin-x64@0.17.19': + optional: true + + '@esbuild/darwin-x64@0.19.2': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.23.1': optional: true + '@esbuild/freebsd-arm64@0.17.19': + optional: true + + '@esbuild/freebsd-arm64@0.19.2': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true + '@esbuild/freebsd-x64@0.17.19': + optional: true + + '@esbuild/freebsd-x64@0.19.2': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true + '@esbuild/linux-arm64@0.17.19': + optional: true + + '@esbuild/linux-arm64@0.19.2': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.23.1': optional: true + '@esbuild/linux-arm@0.17.19': + optional: true + + '@esbuild/linux-arm@0.19.2': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.23.1': optional: true + '@esbuild/linux-ia32@0.17.19': + optional: true + + '@esbuild/linux-ia32@0.19.2': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.23.1': optional: true + '@esbuild/linux-loong64@0.17.19': + optional: true + + '@esbuild/linux-loong64@0.19.2': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.23.1': optional: true + '@esbuild/linux-mips64el@0.17.19': + optional: true + + '@esbuild/linux-mips64el@0.19.2': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true + '@esbuild/linux-ppc64@0.17.19': + optional: true + + '@esbuild/linux-ppc64@0.19.2': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true + '@esbuild/linux-riscv64@0.17.19': + optional: true + + '@esbuild/linux-riscv64@0.19.2': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true + '@esbuild/linux-s390x@0.17.19': + optional: true + + '@esbuild/linux-s390x@0.19.2': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.23.1': optional: true - '@esbuild/linux-x64@0.21.5': + '@esbuild/linux-x64@0.17.19': + optional: true + + '@esbuild/linux-x64@0.19.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.17.19': optional: true - '@esbuild/linux-x64@0.23.1': + '@esbuild/netbsd-x64@0.19.2': optional: true '@esbuild/netbsd-x64@0.21.5': @@ -10635,30 +11717,60 @@ snapshots: '@esbuild/openbsd-arm64@0.23.1': optional: true + '@esbuild/openbsd-x64@0.17.19': + optional: true + + '@esbuild/openbsd-x64@0.19.2': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true + '@esbuild/sunos-x64@0.17.19': + optional: true + + '@esbuild/sunos-x64@0.19.2': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.23.1': optional: true + '@esbuild/win32-arm64@0.17.19': + optional: true + + '@esbuild/win32-arm64@0.19.2': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.23.1': optional: true + '@esbuild/win32-ia32@0.17.19': + optional: true + + '@esbuild/win32-ia32@0.19.2': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.23.1': optional: true + '@esbuild/win32-x64@0.17.19': + optional: true + + '@esbuild/win32-x64@0.19.2': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true @@ -11529,6 +12641,121 @@ snapshots: transitivePeerDependencies: - supports-color + '@modern-js/core@2.60.6': + dependencies: + '@modern-js/node-bundle-require': 2.60.6 + '@modern-js/plugin': 2.60.6 + '@modern-js/utils': 2.60.6 + '@swc/helpers': 0.5.13 + + '@modern-js/module-tools@2.60.6(typescript@5.8.3)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@ast-grep/napi': 0.16.0 + '@babel/core': 7.26.10 + '@babel/types': 7.28.1 + '@modern-js/core': 2.60.6 + '@modern-js/plugin': 2.60.6 + '@modern-js/plugin-changeset': 2.60.6 + '@modern-js/plugin-i18n': 2.60.6 + '@modern-js/swc-plugins': 0.6.11(@swc/helpers@0.5.13) + '@modern-js/types': 2.60.6 + '@modern-js/utils': 2.60.6 + '@rollup/pluginutils': 4.1.1 + '@swc/helpers': 0.5.13 + convert-source-map: 1.8.0 + enhanced-resolve: 5.12.0 + esbuild: 0.19.2 + magic-string: 0.30.12 + postcss: 8.5.1 + postcss-modules: 4.3.1(postcss@8.5.1) + safe-identifier: 0.4.2 + source-map: 0.7.4 + style-inject: 0.3.0 + sucrase: 3.29.0 + tapable: 2.2.1 + terser: 5.43.1 + tsconfig-paths-webpack-plugin: 4.1.0 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - debug + - supports-color + + '@modern-js/node-bundle-require@2.60.6': + dependencies: + '@modern-js/utils': 2.60.6 + '@swc/helpers': 0.5.13 + esbuild: 0.17.19 + + '@modern-js/plugin-changeset@2.60.6': + dependencies: + '@changesets/cli': 2.29.5 + '@changesets/git': 2.0.0 + '@changesets/read': 0.5.9 + '@modern-js/plugin-i18n': 2.60.6 + '@modern-js/utils': 2.60.6 + '@swc/helpers': 0.5.13 + axios: 1.9.0 + resolve-from: 5.0.0 + transitivePeerDependencies: + - debug + + '@modern-js/plugin-i18n@2.60.6': + dependencies: + '@modern-js/utils': 2.60.6 + '@swc/helpers': 0.5.13 + + '@modern-js/plugin@2.60.6': + dependencies: + '@modern-js/utils': 2.60.6 + '@swc/helpers': 0.5.13 + + '@modern-js/swc-plugins-darwin-arm64@0.6.11': + optional: true + + '@modern-js/swc-plugins-darwin-x64@0.6.11': + optional: true + + '@modern-js/swc-plugins-linux-arm64-gnu@0.6.11': + optional: true + + '@modern-js/swc-plugins-linux-arm64-musl@0.6.11': + optional: true + + '@modern-js/swc-plugins-linux-x64-gnu@0.6.11': + optional: true + + '@modern-js/swc-plugins-linux-x64-musl@0.6.11': + optional: true + + '@modern-js/swc-plugins-win32-arm64-msvc@0.6.11': + optional: true + + '@modern-js/swc-plugins-win32-x64-msvc@0.6.11': + optional: true + + '@modern-js/swc-plugins@0.6.11(@swc/helpers@0.5.13)': + optionalDependencies: + '@modern-js/swc-plugins-darwin-arm64': 0.6.11 + '@modern-js/swc-plugins-darwin-x64': 0.6.11 + '@modern-js/swc-plugins-linux-arm64-gnu': 0.6.11 + '@modern-js/swc-plugins-linux-arm64-musl': 0.6.11 + '@modern-js/swc-plugins-linux-x64-gnu': 0.6.11 + '@modern-js/swc-plugins-linux-x64-musl': 0.6.11 + '@modern-js/swc-plugins-win32-arm64-msvc': 0.6.11 + '@modern-js/swc-plugins-win32-x64-msvc': 0.6.11 + '@swc/helpers': 0.5.13 + + '@modern-js/types@2.60.6': {} + + '@modern-js/utils@2.60.6': + dependencies: + '@swc/helpers': 0.5.13 + caniuse-lite: 1.0.30001727 + lodash: 4.17.21 + rslog: 1.2.3 + '@module-federation/error-codes@0.14.0': {} '@module-federation/error-codes@0.17.1': {} @@ -12152,6 +13379,11 @@ snapshots: '@remix-run/router@1.23.0': {} + '@rollup/pluginutils@4.1.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + '@rollup/rollup-android-arm-eabi@4.24.3': optional: true @@ -12238,6 +13470,12 @@ snapshots: deepmerge: 4.3.1 reduce-configs: 1.1.0 + '@rsbuild/plugin-less@1.2.4(@rsbuild/core@1.4.15)': + dependencies: + '@rsbuild/core': 1.4.15 + deepmerge: 4.3.1 + reduce-configs: 1.1.0 + '@rsbuild/plugin-less@1.4.0(@rsbuild/core@1.4.15)': dependencies: '@rsbuild/core': 1.4.15 @@ -12338,6 +13576,20 @@ snapshots: - typescript - webpack-hot-middleware + '@rsbuild/plugin-svgr@1.2.0(@rsbuild/core@1.4.15)(typescript@5.8.3)': + dependencies: + '@rsbuild/core': 1.4.15 + '@rsbuild/plugin-react': 1.3.1(@rsbuild/core@1.4.15) + '@svgr/core': 8.1.0(typescript@5.8.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3) + deepmerge: 4.3.1 + loader-utils: 3.3.1 + transitivePeerDependencies: + - supports-color + - typescript + - webpack-hot-middleware + '@rsbuild/plugin-svgr@1.2.2(@rsbuild/core@1.4.15)(typescript@5.8.3)': dependencies: '@rsbuild/core': 1.4.15 @@ -12951,6 +14203,10 @@ snapshots: transitivePeerDependencies: - typescript + '@swc/helpers@0.5.13': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -13190,10 +14446,6 @@ snapshots: '@types/node@16.9.1': {} - '@types/node@18.19.118': - dependencies: - undici-types: 5.26.5 - '@types/node@18.19.62': dependencies: undici-types: 5.26.5 @@ -13286,6 +14538,13 @@ snapshots: react: 19.1.0 unhead: 2.0.14 + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 + '@vitest/expect@3.0.5': dependencies: '@vitest/spy': 3.0.5 @@ -13293,6 +14552,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(vite@5.4.10(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.10(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1) + '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1))': dependencies: '@vitest/spy': 3.0.5 @@ -13309,6 +14576,10 @@ snapshots: optionalDependencies: vite: 5.4.10(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.0.5': dependencies: tinyrainbow: 2.0.0 @@ -13317,21 +14588,42 @@ snapshots: dependencies: tinyrainbow: 2.0.0 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.0.5': dependencies: '@vitest/utils': 3.0.5 pathe: 2.0.3 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.17 + pathe: 1.1.2 + '@vitest/snapshot@3.0.5': dependencies: '@vitest/pretty-format': 3.0.5 magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.0.5': dependencies: tinyspy: 3.0.2 + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.3 + tinyrainbow: 1.2.0 + '@vitest/utils@3.0.5': dependencies: '@vitest/pretty-format': 3.0.5 @@ -13667,6 +14959,8 @@ snapshots: any-base@1.1.0: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -14282,6 +15576,8 @@ snapshots: commander@2.20.3: {} + commander@4.1.1: {} + commander@7.2.0: {} commitizen@4.2.5(@types/node@22.15.3)(typescript@5.8.3): @@ -14384,6 +15680,10 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + convert-source-map@1.8.0: + dependencies: + safe-buffer: 5.1.2 + convert-source-map@2.0.0: {} cookie-signature@1.0.6: {} @@ -14531,6 +15831,8 @@ snapshots: css-what@6.2.2: {} + cssesc@3.0.0: {} + csso@5.0.5: dependencies: css-tree: 2.2.1 @@ -14572,6 +15874,8 @@ snapshots: dargs@8.1.0: {} + data-uri-to-buffer@4.0.1: {} + data-uri-to-buffer@6.0.2: {} data-urls@5.0.0: @@ -14984,6 +16288,56 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.2 + esbuild@0.17.19: + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + + esbuild@0.19.2: + optionalDependencies: + '@esbuild/android-arm': 0.19.2 + '@esbuild/android-arm64': 0.19.2 + '@esbuild/android-x64': 0.19.2 + '@esbuild/darwin-arm64': 0.19.2 + '@esbuild/darwin-x64': 0.19.2 + '@esbuild/freebsd-arm64': 0.19.2 + '@esbuild/freebsd-x64': 0.19.2 + '@esbuild/linux-arm': 0.19.2 + '@esbuild/linux-arm64': 0.19.2 + '@esbuild/linux-ia32': 0.19.2 + '@esbuild/linux-loong64': 0.19.2 + '@esbuild/linux-mips64el': 0.19.2 + '@esbuild/linux-ppc64': 0.19.2 + '@esbuild/linux-riscv64': 0.19.2 + '@esbuild/linux-s390x': 0.19.2 + '@esbuild/linux-x64': 0.19.2 + '@esbuild/netbsd-x64': 0.19.2 + '@esbuild/openbsd-x64': 0.19.2 + '@esbuild/sunos-x64': 0.19.2 + '@esbuild/win32-arm64': 0.19.2 + '@esbuild/win32-ia32': 0.19.2 + '@esbuild/win32-x64': 0.19.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -15099,6 +16453,8 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 3.0.3 + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -15293,6 +16649,11 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -15426,6 +16787,10 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -15494,6 +16859,10 @@ snapshots: functions-have-names@1.2.3: {} + generic-names@4.0.0: + dependencies: + loader-utils: 3.3.1 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -15595,6 +16964,15 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -15955,6 +17333,8 @@ snapshots: human-id@1.0.2: {} + human-id@4.1.1: {} + human-signals@1.1.1: {} human-signals@7.0.0: {} @@ -15975,6 +17355,12 @@ snapshots: dependencies: safer-buffer: 2.1.2 + icss-replace-symbols@1.1.0: {} + + icss-utils@5.1.0(postcss@8.5.1): + dependencies: + postcss: 8.5.1 + ieee754@1.2.1: {} ignore-by-default@1.0.1: {} @@ -16326,7 +17712,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 18.19.118 + '@types/node': 22.15.3 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -16544,7 +17930,6 @@ snapshots: mime: 1.6.0 needle: 3.3.1 source-map: 0.6.1 - optional: true lie@3.3.0: dependencies: @@ -16723,6 +18108,10 @@ snapshots: dependencies: react: 18.3.1 + magic-string@0.30.12: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -17349,6 +18738,12 @@ snapshots: ncp: 2.0.0 rimraf: 2.4.5 + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nano-staged@0.8.0: dependencies: picocolors: 1.1.1 @@ -17382,6 +18777,12 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-machine-id@1.1.12: {} node-releases@2.0.19: {} @@ -17692,6 +19093,10 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.10 + pako@1.0.11: {} parent-module@1.0.1: @@ -17791,6 +19196,8 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} pathval@2.0.0: {} @@ -17823,6 +19230,8 @@ snapshots: pify@4.0.1: {} + pirates@4.0.7: {} + pixelmatch@4.0.2: dependencies: pngjs: 3.4.0 @@ -17884,6 +19293,46 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss-modules-extract-imports@3.1.0(postcss@8.5.1): + dependencies: + postcss: 8.5.1 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.1): + dependencies: + icss-utils: 5.1.0(postcss@8.5.1) + postcss: 8.5.1 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.1): + dependencies: + postcss: 8.5.1 + postcss-selector-parser: 7.1.0 + + postcss-modules-values@4.0.0(postcss@8.5.1): + dependencies: + icss-utils: 5.1.0(postcss@8.5.1) + postcss: 8.5.1 + + postcss-modules@4.3.1(postcss@8.5.1): + dependencies: + generic-names: 4.0.0 + icss-replace-symbols: 1.1.0 + lodash.camelcase: 4.3.0 + postcss: 8.5.1 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.1) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.1) + postcss-modules-scope: 3.2.1(postcss@8.5.1) + postcss-modules-values: 4.0.0(postcss@8.5.1) + string-hash: 1.1.3 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + postcss@8.5.1: dependencies: nanoid: 3.3.8 @@ -18015,6 +19464,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.10: {} + query-string@9.1.1: dependencies: decode-uri-component: 0.4.1 @@ -18824,6 +20275,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-identifier@0.4.2: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -19344,6 +20797,11 @@ snapshots: cross-spawn: 5.1.0 signal-exit: 3.0.7 + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -19413,6 +20871,8 @@ snapshots: string-convert@0.2.1: {} + string-hash@1.1.3: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -19492,6 +20952,8 @@ snapshots: '@tokenizer/token': 0.3.0 peek-readable: 4.1.0 + style-inject@0.3.0: {} + style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -19502,6 +20964,15 @@ snapshots: stylis@4.3.4: {} + sucrase@3.29.0: + dependencies: + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -19617,6 +21088,14 @@ snapshots: text-extensions@2.4.0: {} + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thingies@1.21.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -19652,6 +21131,8 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@3.0.2: {} @@ -19728,6 +21209,8 @@ snapshots: optionalDependencies: '@rspack/core': 1.4.11(@swc/helpers@0.5.17) + ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@18.19.62)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -19746,6 +21229,12 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tsconfig-paths-webpack-plugin@4.1.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.2 + tsconfig-paths: 4.2.0 + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -20000,6 +21489,24 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + vite-node@2.1.9(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1): + dependencies: + cac: 6.7.14 + debug: 4.4.0(supports-color@5.5.0) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.10(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.0.5(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1): dependencies: cac: 6.7.14 @@ -20062,6 +21569,42 @@ snapshots: sass-embedded: 1.86.3 terser: 5.43.1 + vitest@2.1.9(@types/node@22.15.3)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.10(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.0(supports-color@5.5.0) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.10(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1) + vite-node: 2.1.9(@types/node@22.15.3)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.3 + jsdom: 26.1.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.43.1): dependencies: '@vitest/expect': 3.0.5 @@ -20158,6 +21701,8 @@ snapshots: web-namespaces@2.0.1: {} + web-streams-polyfill@3.3.3: {} + web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {}