This is page 1 of 2. Use http://codebase.md/inditextech/mcp-server-simulator-ios-idb?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── PR-verify.yml
│ └── sonar-branch-analysis.yml
├── .gitignore
├── .npmrc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│ └── demo.gif
├── glama.json
├── jest.config.js
├── LICENSE
├── LICENSES
│ ├── Apache-2.0.txt
│ └── CC-BY-SA-4.0.txt
├── package-lock.json
├── package.json
├── README.md
├── repolinter.json
├── REUSE.toml
├── scripts
│ └── install_dependencies.sh
├── SECURITY.md
├── src
│ ├── adapters
│ │ ├── OrchestratorToIDB.ts
│ │ └── ParserToOrchestrator.ts
│ ├── idb
│ │ ├── IDBManager.ts
│ │ └── interfaces
│ │ └── IIDBManager.ts
│ ├── index.ts
│ ├── mcp
│ │ └── mcp-server.ts
│ ├── orchestrator
│ │ ├── __tests__
│ │ │ └── MCPOrchestrator.test.ts
│ │ ├── interfaces
│ │ │ └── IOrchestratorCommand.ts
│ │ └── MCPOrchestrator.ts
│ └── parser
│ ├── __tests__
│ │ └── NLParser.test.ts
│ ├── commands
│ │ ├── AccessibilityCommands.ts
│ │ ├── AppCommands.ts
│ │ ├── BaseCommandDefinition.ts
│ │ ├── CaptureCommands.ts
│ │ ├── CommandRegistry.ts
│ │ ├── DebugCommands.ts
│ │ ├── MiscCommands.ts
│ │ ├── SimulatorCommands.ts
│ │ └── UICommands.ts
│ ├── interfaces
│ │ └── IParser.ts
│ └── NLParser.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
```
1 | registry=https://registry.npmjs.org/
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | venv/
4 |
5 | # Build output
6 | dist/
7 | build/
8 |
9 | # Logs
10 | logs/
11 | *.log
12 | npm-debug.log*
13 |
14 | # Environment variables
15 | .env
16 | .env.local
17 | .env.*.local
18 |
19 | # IDEs and editors
20 | .idea/
21 | .vscode/
22 | *.swp
23 | *.swo
24 |
25 | # System files
26 | .DS_Store
27 | Thumbs.db
28 |
29 | # Python
30 | __pycache__/
31 | *.py[cod]
32 | *$py.class
33 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # 📱 MCP Server for iOS Simulator
2 |
3 | [](https://glama.ai/mcp/servers/@InditexTech/mcp-server-simulator-ios-idb)
4 |
5 | A Model Context Protocol (MCP) server that enables LLMs to interact with iOS simulators through natural language commands.
6 |
7 | ## ℹ️ Overview
8 |
9 | This MCP server provides a bridge between Large Language Models (LLMs) and iOS simulators, offering comprehensive control through natural language commands. Here's what it can do:
10 |
11 | For detailed usage, see the Installation guide and Supported Commands sections. You can use this server either through direct MCP integration or as a standalone library.
12 |
13 | Check out the Architecture section to understand how the components work together to enable natural language control of iOS simulators.
14 |
15 | 
16 |
17 | ### 🎮 Simulator Control
18 | - Create and manage simulator sessions
19 | - Boot, shutdown, and monitor simulator states
20 | - List available and running simulators
21 | - Focus simulator windows
22 |
23 | ### 📱 Application Management
24 | - Install and manage iOS applications
25 | - Launch, terminate, and uninstall apps
26 | - Monitor app states and verify installations
27 | - Handle app permissions and configurations
28 |
29 | ### 🖱️ UI Interaction & Testing
30 | - Interact with the simulator UI
31 | - Execute tap, swipe, and button press actions
32 | - Input text and key sequences
33 | - Access accessibility elements for UI testing
34 | - Record videos of UI interactions
35 |
36 | ### 🛠️ Development & Debugging
37 | - Capture screenshots and system logs
38 | - Debug applications in real-time
39 | - Monitor and analyze crash logs
40 | - Install dynamic libraries and manage app data
41 |
42 | ### ⚡ Advanced Features
43 | - Additional functionality includes:
44 | - Location simulation
45 | - Media injection
46 | - URL scheme handling
47 | - Contact database management
48 | - Keychain operations
49 |
50 | For detailed usage, see the Installation guide and Supported Commands sections. You can use this server either through direct MCP integration or as a standalone library.
51 |
52 | Check out the Architecture section to understand how the components work together to enable natural language control of iOS simulators.
53 |
54 | ## 📋 Requirements
55 |
56 | - **macOS**: Required for iOS simulator support
57 | - **Node.js**: v14.0.0 or higher
58 | - **Homebrew**: Required for installing dependencies
59 | - **XCode**: With iOS simulators installed
60 |
61 | ## 🚀 Installation
62 |
63 | The easiest way to install this server is through Cline:
64 |
65 | 1. Simply ask Cline:
66 | ```
67 | Add this mcp to cline https://github.com/InditexTech/mcp-server-simulator-ios-idb
68 | ```
69 |
70 | 2. Cline will handle the installation process automatically, including dependency management and configuration.
71 |
72 | Alternatively, you can install it manually:
73 |
74 | ```bash
75 | # Clone the repository
76 | git clone https://github.com/InditexTech/mcp-server-simulator-ios-idb.git
77 | cd mcp-server-simulator-ios-idb
78 |
79 | # Create and activate Python virtual environment
80 | python3 -m venv venv
81 | source venv/bin/activate # On Unix/macOS
82 |
83 | # Install dependencies
84 | npm install
85 |
86 | # Build the project
87 | npm run build
88 |
89 | # Start the project
90 | npm start
91 |
92 | # Run tests
93 | npm test
94 | ```
95 |
96 | The installation process will automatically:
97 | 1. Check if you're running macOS
98 | 2. Install idb-companion via Homebrew
99 | 3. Install fb-idb via pip in the virtual environment
100 |
101 | Note: Make sure to keep the virtual environment activated while using the server. If you close your terminal and come back later, you'll need to reactivate the virtual environment with the `source venv/bin/activate` command before running `npm start`.
102 |
103 | ## 🔌 MCP Integration
104 |
105 | To use this server with Claude or other LLM assistants:
106 |
107 | 1. Add the server to your MCP settings in Claude Desktop:
108 |
109 | ```json
110 | {
111 | "mcpServers": {
112 | "ios-simulator": {
113 | "command": "node",
114 | "args": ["/path/to/mcp-server-simulator-ios-idb/dist/index.js"],
115 | "env": {}
116 | }
117 | }
118 | }
119 | ```
120 |
121 | 2. The LLM can now use natural language commands to control iOS simulators:
122 |
123 | ```
124 | create a simulator session with iPhone 14
125 | install app /path/to/my-app.ipa
126 | launch app com.example.myapp
127 | tap at 100, 200
128 | take a screenshot
129 | ```
130 |
131 | ## 📚 Usage as a Library
132 |
133 | You can also use this package as a library in your own projects:
134 |
135 | ### 🔰 Basic Usage
136 |
137 | ```typescript
138 | import { createMCPServer } from 'mcp-server-simulator-ios-idb';
139 |
140 | async function main() {
141 | // Create an instance of the MCP server
142 | const { orchestrator } = createMCPServer();
143 |
144 | // Process natural language commands
145 |
146 | // Create a simulator session
147 | const sessionResult = await orchestrator.processInstruction('create session');
148 | console.log(`Session created: ${sessionResult.data}`);
149 |
150 | // Interact with the simulator
151 | await orchestrator.processInstruction('tap at 100, 200');
152 |
153 | // Capture a screenshot
154 | const screenshotResult = await orchestrator.processInstruction('take screenshot');
155 | console.log(`Screenshot saved at: ${screenshotResult.data}`);
156 | }
157 |
158 | main().catch(console.error);
159 | ```
160 |
161 | ### 🚀 Advanced Usage
162 |
163 | You can also use the individual components directly:
164 |
165 | ```typescript
166 | import {
167 | IDBManager,
168 | NLParser,
169 | MCPOrchestrator,
170 | ParserToOrchestrator,
171 | OrchestratorToIDB
172 | } from 'mcp-server-simulator-ios-idb';
173 |
174 | // Create instances
175 | const idbManager = new IDBManager();
176 | const parser = new NLParser();
177 | const orchestrator = new MCPOrchestrator(parser, idbManager);
178 |
179 | // Use the components directly
180 | const sessionId = await idbManager.createSimulatorSession({
181 | deviceName: 'iPhone 12',
182 | platformVersion: '15.0'
183 | });
184 |
185 | await idbManager.tap(sessionId, 100, 200);
186 | ```
187 |
188 | ## 🏗️ Project Structure
189 |
190 | ```
191 | mcp-server-simulator-ios-idb/
192 | ├── src/ # Source code
193 | │ ├── adapters/ # Adapter components
194 | │ ├── idb/ # IDB manager implementation
195 | │ ├── mcp/ # MCP server implementation
196 | │ ├── orchestrator/ # Command orchestrator
197 | │ ├── parser/ # Natural language parser
198 | │ └── index.ts # Main entry point
199 | ├── types/ # TypeScript type definitions
200 | ├── scripts/ # Installation scripts
201 | ├── package.json # Project configuration
202 | └── tsconfig.json # TypeScript configuration
203 | ```
204 |
205 | ## 🎯 Supported Commands
206 |
207 | The NLParser supports the following natural language commands:
208 |
209 | ### 🎮 Simulator Management
210 | | Command | Description | Example |
211 | |---------|-------------|---------|
212 | | Create session | Creates a new simulator session | "create session", "create simulator iPhone 12" |
213 | | Terminate session | Terminates the current session | "terminate session", "close simulator" |
214 | | List simulators | Lists available simulators | "list simulators", "show simulators" |
215 | | List booted simulators | Lists running simulators | "list booted simulators", "show running simulators" |
216 | | Boot simulator | Boots a simulator by UDID | "boot simulator 5A321B8F-4D85-4267-9F79-2F5C91D142C2" |
217 | | Shutdown simulator | Shuts down a simulator | "shutdown simulator 5A321B8F-4D85-4267-9F79-2F5C91D142C2" |
218 | | Focus simulator | Brings simulator window to front | "focus simulator", "bring simulator to front" |
219 | | List simulator sessions | Lists active simulator sessions | "list simulator sessions", "show active sessions" |
220 |
221 | ### 📱 App Management
222 | | Command | Description | Example |
223 | |---------|-------------|---------|
224 | | Install app | Installs an app on the simulator | "install app /path/to/app.ipa" |
225 | | Launch app | Launches an app on the simulator | "launch app com.example.app" |
226 | | Terminate app | Terminates a running app | "terminate app com.example.app" |
227 | | Uninstall app | Uninstalls an app | "uninstall app com.example.app" |
228 | | List apps | Lists installed applications | "list apps", "show installed apps" |
229 | | Check if app installed | Checks if an app is installed | "is app com.example.app installed" |
230 |
231 | ### 🖱️ UI Interaction
232 | | Command | Description | Example |
233 | |---------|-------------|---------|
234 | | Tap | Taps at specific coordinates | "tap at 100, 200" |
235 | | Swipe | Performs a swipe gesture | "swipe from 100, 200 to 300, 400" |
236 | | Press button | Presses a device button | "press button HOME", "press button SIRI" |
237 | | Input text | Types text | "input text Hello World" |
238 | | Press key | Presses a key by code | "press key 4" |
239 | | Press key sequence | Presses a sequence of keys | "press key sequence 4 5 6" |
240 |
241 | ### ♿ Accessibility
242 | | Command | Description | Example |
243 | |---------|-------------|---------|
244 | | Describe elements | Lists all accessibility elements | "describe all elements", "show accessibility elements" |
245 | | Describe point | Describes element at coordinates | "describe point 100, 200", "what's at 150, 300" |
246 |
247 | ### 📸 Capture and Logs
248 | | Command | Description | Example |
249 | |---------|-------------|---------|
250 | | Take screenshot | Captures a screenshot | "take screenshot", "capture screen" |
251 | | Record video | Records screen activity | "record video /path/output.mp4" |
252 | | Stop recording | Stops video recording | "stop recording", "stop video recording" |
253 | | Get logs | Retrieves system or app logs | "get logs", "get logs for com.example.app" |
254 |
255 | ### 🐛 Debug
256 | | Command | Description | Example |
257 | |---------|-------------|---------|
258 | | Start debug | Starts a debug session | "debug app com.example.app", "start debug com.example.app" |
259 | | Stop debug | Stops a debug session | "stop debug", "terminate debug session" |
260 | | Debug status | Gets debug session status | "debug status", "show debug info" |
261 |
262 | ### 💥 Crash Logs
263 | | Command | Description | Example |
264 | |---------|-------------|---------|
265 | | List crash logs | Lists available crash logs | "list crash logs", "show crash logs" |
266 | | Show crash log | Shows content of a crash log | "show crash log crash_2023-01-01" |
267 | | Delete crash logs | Deletes crash logs | "delete crash logs", "clear crash logs" |
268 |
269 | ### 🔧 Additional Commands
270 | | Command | Description | Example |
271 | |---------|-------------|---------|
272 | | Install dylib | Installs a dynamic library | "install dylib /path/to/library.dylib" |
273 | | Open URL | Opens a URL in the simulator | "open url https://example.com" |
274 | | Clear keychain | Clears the simulator's keychain | "clear keychain" |
275 | | Set location | Sets the simulator's location | "set location 37.7749, -122.4194" |
276 | | Add media | Adds media to the camera roll | "add media /path/to/image.jpg" |
277 | | Approve permissions | Approves app permissions | "approve permissions com.example.app photos camera" |
278 | | Update contacts | Updates contacts database | "update contacts /path/to/contacts.sqlite" |
279 |
280 | The interface supports all commands available in the idb CLI tool, providing a comprehensive set of operations for iOS simulator automation.
281 |
282 | ## 🔍 Architecture
283 |
284 | The server consists of three main components:
285 |
286 | 1. **IDBManager**: Low-level component that interacts directly with iOS simulators through idb.
287 | 2. **NLParser**: Component that interprets natural language instructions and converts them into structured commands.
288 | 3. **MCPOrchestrator**: Central component that coordinates interactions between the parser and the IDBManager.
289 |
290 | These components are connected through adapters:
291 | - **ParserToOrchestrator**: Converts parser results into orchestrator commands.
292 | - **OrchestratorToIDB**: Translates orchestrator commands into IDBManager calls.
293 |
294 | ## 🔌 MCP Integration
295 |
296 | To use this server with the Model Context Protocol:
297 |
298 | 1. Add the server to your MCP settings:
299 |
300 | ```json
301 | {
302 | "mcpServers": {
303 | "ios-simulator": {
304 | "command": "node",
305 | "args": ["/path/to/mcp-server-simulator-ios-idb/dist/index.js"],
306 | "env": {}
307 | }
308 | }
309 | }
310 | ```
311 |
312 | 2. Connect to the server in your LLM application:
313 |
314 | ```typescript
315 | const result = await useMcpTool({
316 | serverName: "ios-simulator",
317 | toolName: "process-instruction",
318 | arguments: {
319 | instruction: "create simulator session"
320 | }
321 | });
322 | ```
323 |
324 | ## 🙏 Acknowledgments
325 |
326 | This project would not be possible without [facebook/idb](https://github.com/facebook/idb), which provides the underlying iOS simulator control capabilities. We extend our sincere gratitude to the Facebook/Meta team and all contributors to the idb project for creating and maintaining such a powerful and reliable tool.
327 |
328 | ## 📄 License
329 |
330 | This tool is available as open source under the terms of the Apache-2.0.
331 |
```
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
```markdown
1 | <!--
2 | SPDX-FileCopyrightText: 2025 Industria de Diseño Textil S.A. INDITEX
3 |
4 | SPDX-License-Identifier: CC-BY-SA-4.0
5 | -->
6 |
7 | # Security
8 |
9 | We at Inditex believe that responsible disclosure of security vulnerabilities helps us ensure the security and privacy of all opensource community.
10 |
11 | If you believe you have found a security vulnerability in any Inditex repository that meets Inditex definition of a security vulnerability, please report it to us as described below. We appreciate the hard work maintainers put into fixing vulnerabilities and understand that sometimes more time is required to properly address an issue.
12 |
13 | ## Reporting security issues
14 |
15 | > [!CAUTION]
16 | > Do not file public issues on GitHub for security vulnerabilities
17 |
18 | * Let us know by submitting the finding through our [disclosure submission program](https://vdp.inditex.com) as soon as possible, upon discovery of a potential security issue.
19 | * Once we've assessed your report, we will create a GitHub "security advisory", which will allow the reporter and Inditex team to work on the issue in a confidential manner. We will invite you as a collaborator to the advisory and any needed trusted persons.
20 | * That "security advisory" will also allow us to have a temporary private fork, to work on the fix in confidentiality.
21 | * Once a fix is ready, we will include the fix in our next release and mark that release as a security release.
22 | * Details on the issue will be embargoed for 30 days to give users an oppurtunity to upgrade, after which we will coordinate disclosure with the researcher(s).
23 | * If you've contributed the fix, you will be credited for it.
24 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | <!--
2 | SPDX-FileCopyrightText: 2024 Industria de Diseño Textil S.A. INDITEX
3 |
4 | SPDX-License-Identifier: CC-BY-SA-4.0
5 | -->
6 |
7 | # Contributing
8 |
9 | Thank you for your interest in contributing to this project! We value and appreciate any contributions you can make.
10 | To maintain a collaborative and respectful environment, please consider the following guidelines when contributing to
11 | this project.
12 |
13 | ## Prerequisites
14 |
15 | - Before starting to contribute to the code, you must first sign the [Contributor License Agreement (CLA)](https://github.com/InditexTech/foss/blob/main/documents/ITX_OSS_CLA.pdf).
16 |
17 | ## How to Contribute
18 |
19 | 1. Open an issue to discuss and gather feedback on the feature or fix you wish to address.
20 | 2. Fork the repository and clone it to your local machine.
21 | 3. Create a new branch to work on your contribution: `git checkout -b your-branch-name`.
22 | 4. Make the necessary changes in your local branch.
23 | 5. Ensure that your code follows the established project style and formatting guidelines.
24 | 6. Perform testing to ensure your changes do not introduce errors.
25 | 7. Make clear and descriptive commits that explain your changes.
26 | 8. Push your branch to the remote repository: `git push origin your-branch-name`.
27 | 9. Open a pull request describing your changes and linking the corresponding issue.
28 | 10. Await comments and discussions on your pull request. Make any necessary modifications based on the received feedback.
29 | 11. Once your pull request is approved, your contribution will be merged into the main branch.
30 |
31 | ## Contribution Guidelines
32 |
33 | - All contributors are expected to follow the project's [code of conduct](CODE_of_CONDUCT.md). Please be respectful and
34 | considerate towards other contributors.
35 | - Before starting work on a new feature or fix, check existing [issues](../../issues) and [pull requests](../../pulls)
36 | to avoid duplications and unnecessary discussions.
37 | - If you wish to work on an existing issue, comment on the issue to inform other contributors that you are working on it.
38 | This will help coordinate efforts and prevent conflicts.
39 | - It is always advisable to discuss and gather feedback from the community before making significant changes to the
40 | project's structure or architecture.
41 | - Ensure a clean and organized commit history. Divide your changes into logical and descriptive commits. We recommend to use the [Conventional Commits Specification](https://www.conventionalcommits.org/en/v1.0.0/)
42 | - Document any new changes or features you add. This will help other contributors and project users understand your work
43 | and its purpose.
44 | - Be sure to link the corresponding issue in your pull request to maintain proper tracking of contributions.
45 | - Remember to add license and copyright information following the [REUSE Specification](https://reuse.software/spec/#copyright-and-licensing-information).
46 |
```
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
```markdown
1 | <!--
2 | SPDX-FileCopyrightText: 2024 Industria de Diseño Textil S.A. INDITEX
3 |
4 | SPDX-License-Identifier: CC-BY-SA-4.0
5 | -->
6 |
7 | # Contributor Covenant Code of Conduct
8 |
9 | > Note: This Code of Conduct is a baseline for this project and it doesn't overpass
10 | current [Inditex's corporate ethics](https://www.inditex.com/itxcomweb/en/group/our-ethical-commitment).
11 |
12 | ## Our Pledge
13 |
14 | We as members, contributors, and leaders pledge to make participation in our
15 | community a harassment-free experience for everyone, regardless of age, body
16 | size, visible or invisible disability, ethnicity, sex characteristics, gender
17 | identity and expression, level of experience, education, socio-economic status,
18 | nationality, personal appearance, race, caste, color, religion, or sexual
19 | identity and orientation.
20 |
21 | We pledge to act and interact in ways that contribute to an open, welcoming,
22 | diverse, inclusive, and healthy community.
23 |
24 | ## Our Standards
25 |
26 | Examples of behavior that contributes to a positive environment for our
27 | community include:
28 |
29 | * Demonstrating empathy and kindness toward other people
30 | * Being respectful of differing opinions, viewpoints, and experiences
31 | * Giving and gracefully accepting constructive feedback
32 | * Accepting responsibility and apologizing to those affected by our mistakes,
33 | and learning from the experience
34 | * Focusing on what is best not just for us as individuals, but for the overall
35 | community
36 |
37 | Examples of unacceptable behavior include:
38 |
39 | * The use of sexualized language or imagery, and sexual attention or advances of
40 | any kind
41 | * Trolling, insulting or derogatory comments, and personal or political attacks
42 | * Public or private harassment
43 | * Publishing others' private information, such as a physical or email address,
44 | without their explicit permission
45 | * Other conduct which could reasonably be considered inappropriate in a
46 | professional setting
47 |
48 | ## Enforcement Responsibilities
49 |
50 | Community leaders are responsible for clarifying and enforcing our standards of
51 | acceptable behavior and will take appropriate and fair corrective action in
52 | response to any behavior that they deem inappropriate, threatening, offensive,
53 | or harmful.
54 |
55 | Community leaders have the right and responsibility to remove, edit, or reject
56 | comments, commits, code, wiki edits, issues, and other contributions that are
57 | not aligned to this Code of Conduct, and will communicate reasons for moderation
58 | decisions when appropriate.
59 |
60 | ## Scope
61 |
62 | This Code of Conduct applies within all community spaces, and also applies when
63 | an individual is officially representing the community in public spaces.
64 | Examples of representing our community include using an official email address,
65 | posting via an official social media account, or acting as an appointed
66 | representative at an online or offline event.
67 |
68 | ## Enforcement
69 |
70 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
71 | reported to the community leaders responsible for enforcement at
72 | [[email protected]](mailto:[email protected]).
73 | All complaints will be reviewed and investigated promptly and fairly.
74 |
75 | All community leaders are obligated to respect the privacy and security of the
76 | reporter of any incident.
77 |
78 | ## Enforcement Guidelines
79 |
80 | Community leaders will follow these Community Impact Guidelines in determining
81 | the consequences for any action they deem in violation of this Code of Conduct:
82 |
83 | ### 1. Correction
84 |
85 | **Community Impact**: Use of inappropriate language or other behavior deemed
86 | unprofessional or unwelcome in the community.
87 |
88 | **Consequence**: A private, written warning from community leaders, providing
89 | clarity around the nature of the violation and an explanation of why the
90 | behavior was inappropriate. A public apology may be requested.
91 |
92 | ### 2. Warning
93 |
94 | **Community Impact**: A violation through a single incident or series of
95 | actions.
96 |
97 | **Consequence**: A warning with consequences for continued behavior. No
98 | interaction with the people involved, including unsolicited interaction with
99 | those enforcing the Code of Conduct, for a specified period of time. This
100 | includes avoiding interactions in community spaces as well as external channels
101 | like social media. Violating these terms may lead to a temporary or permanent
102 | ban.
103 |
104 | ### 3. Temporary Ban
105 |
106 | **Community Impact**: A serious violation of community standards, including
107 | sustained inappropriate behavior.
108 |
109 | **Consequence**: A temporary ban from any sort of interaction or public
110 | communication with the community for a specified period of time. No public or
111 | private interaction with the people involved, including unsolicited interaction
112 | with those enforcing the Code of Conduct, is allowed during this period.
113 | Violating these terms may lead to a permanent ban.
114 |
115 | ### 4. Permanent Ban
116 |
117 | **Community Impact**: Demonstrating a pattern of violation of community
118 | standards, including sustained inappropriate behavior, harassment of an
119 | individual, or aggression toward or disparagement of classes of individuals.
120 |
121 | **Consequence**: A permanent ban from any sort of public interaction within the
122 | community.
123 |
124 | ## Attribution
125 |
126 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
127 | version 2.1, available at
128 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
129 |
130 | Community Impact Guidelines were inspired by
131 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
132 |
133 | For answers to common questions about this code of conduct, see the FAQ at
134 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
135 | [https://www.contributor-covenant.org/translations][translations].
136 |
137 | [homepage]: https://www.contributor-covenant.org
138 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
139 | [Mozilla CoC]: https://github.com/mozilla/diversity
140 | [FAQ]: https://www.contributor-covenant.org/faq
141 | [translations]: https://www.contributor-covenant.org/translations
142 |
```
--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://glama.ai/mcp/schemas/server.json",
3 | "maintainers": [
4 | "arturonaredo"
5 | ]
6 | }
7 |
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | export default {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | extensionsToTreatAsEsm: ['.ts'],
6 | moduleNameMapper: {
7 | '^(\\.{1,2}/.*)\\.js$': '$1',
8 | },
9 | transform: {
10 | '^.+\\.tsx?$': ['ts-jest', {
11 | useESM: true,
12 | }],
13 | },
14 | testMatch: [
15 | '**/__tests__/**/*.test.ts',
16 | '!**/*.d.ts'
17 | ],
18 | };
19 |
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "lib": ["ES2020", "DOM"],
7 | "declaration": true,
8 | "outDir": "./dist",
9 | "rootDir": "./src",
10 | "strict": true,
11 | "esModuleInterop": true,
12 | "skipLibCheck": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "resolveJsonModule": true,
15 | "types": ["node", "jest"],
16 | "typeRoots": ["./node_modules/@types", "./types"],
17 | "allowSyntheticDefaultImports": true,
18 | "noImplicitAny": false
19 | },
20 | "ts-node": {
21 | "esm": true
22 | },
23 | "include": [
24 | "src/**/*.ts",
25 | "src/**/__tests__/**/*.ts",
26 | "types/**/*.d.ts"
27 | ],
28 | "exclude": [
29 | "node_modules",
30 | "dist"
31 | ]
32 | }
33 |
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-server-simulator-ios-idb",
3 | "version": "1.0.1",
4 | "description": "Model Context Protocol server for iOS simulator automation via IDB",
5 | "main": "dist/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "NODE_OPTIONS=--experimental-vm-modules jest",
9 | "build": "tsc",
10 | "start": "node dist/index.js",
11 | "postinstall": "chmod +x ./scripts/install_dependencies.sh && ./scripts/install_dependencies.sh"
12 | },
13 | "engines": {
14 | "node": ">=14.0.0",
15 | "os": "darwin"
16 | },
17 | "author": "arturono[at]inditex[dot]com",
18 | "license": "Apache-2.0",
19 | "dependencies": {
20 | "@modelcontextprotocol/sdk": "^1.6.1",
21 | "uuid": "^9.0.0",
22 | "zod": "^3.22.4"
23 | },
24 | "devDependencies": {
25 | "@types/jest": "^29.5.14",
26 | "@types/node": "^20.11.0",
27 | "@types/uuid": "^9.0.0",
28 | "jest": "^29.7.0",
29 | "ts-jest": "^29.2.6",
30 | "typescript": "^5.0.0"
31 | }
32 | }
33 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [1.0.1] - 2025-04-02
9 |
10 | ### Fixed
11 | - Fixed logs directory creation issue where logs were being created in the root directory instead of the application directory
12 | - Updated both index.ts and mcp-server.ts to use ESM equivalent of __dirname instead of process.cwd()
13 |
14 | ## [1.0.0] - 2025-03-20
15 |
16 | ### Added
17 | - Initial release
18 | - Natural Language Parser for IDB commands
19 | - MCP Server implementation
20 | - IDB Manager with complete command support
21 | - Orchestrator for command handling
22 | - Support for all IDB simulator management features
23 | - Extensive test coverage for core components
24 | - Command registry with accessibility, app, capture, debug, misc, simulator and UI commands
25 | - Documentation and examples
26 | - Security policy
27 | - Contribution guidelines
28 | - Code of conduct
29 |
```
--------------------------------------------------------------------------------
/scripts/install_dependencies.sh:
--------------------------------------------------------------------------------
```bash
1 | # SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | # SPDX-License-Identifier: Apache-2.0
3 | #!/bin/bash
4 |
5 | # Dependency installation script for MCP
6 | # This script will run during the MCP package installation
7 |
8 | # Check that we are on macOS
9 | if [[ "$OSTYPE" != "darwin"* ]]; then
10 | echo "Error: This package only works on macOS"
11 | exit 1
12 | fi
13 |
14 | # Check if homebrew is installed
15 | if ! command -v brew &> /dev/null; then
16 | echo "Error: Homebrew is not installed. Please install Homebrew first."
17 | echo "Visit https://brew.sh for installation instructions"
18 | exit 1
19 | fi
20 |
21 | # Install dependencies using Homebrew
22 | echo "Installing idb-companion..."
23 | brew tap facebook/fb
24 | brew install idb-companion
25 |
26 | # Check if we're in a virtual environment
27 | if [[ -z "$VIRTUAL_ENV" ]]; then
28 | echo "Error: Python virtual environment not activated"
29 | echo "Please activate your virtual environment first:"
30 | echo " source venv/bin/activate # On Unix/macOS"
31 | echo " .\\venv\\Scripts\\activate # On Windows"
32 | exit 1
33 | fi
34 |
35 | # Install Python dependencies in virtual environment
36 | echo "Installing idb Python client in virtual environment..."
37 | python3 -m pip install --upgrade pip
38 | python3 -m pip install fb-idb
39 |
40 | # Verify installation
41 | if python3 -m pip show fb-idb &> /dev/null; then
42 | echo "✅ Dependencies installed successfully"
43 | else
44 | echo "❌ Error installing dependencies"
45 | exit 1
46 | fi
47 |
```
--------------------------------------------------------------------------------
/REUSE.toml:
--------------------------------------------------------------------------------
```toml
1 | version = 1
2 | SPDX-PackageName = "mcp-server-simulator-ios-idb"
3 | SPDX-PackageSupplier = "2025 Industria de Diseño Textil S.A."
4 | SPDX-PackageDownloadLocation = "https://github.com/InditexTech/mcp-server-simulator-ios-idb"
5 |
6 | # Computer-generated files.
7 | [[annotations]]
8 | path = [
9 | ".gitignore",
10 | ".npmrc",
11 | "package*.json",
12 | "repolinter.json",
13 | "tsconfig.json",
14 | "jest.config.js",
15 | "glama.json",
16 | ]
17 | SPDX-FileCopyrightText = "2025 Industria Textil de Diseño, S.A."
18 | SPDX-License-Identifier = "Apache-2.0"
19 |
20 | [[annotations]]
21 | path = ".github/ISSUE_TEMPLATE/**"
22 | SPDX-FileCopyrightText = "2025 Industria Textil de Diseño, S.A."
23 | SPDX-License-Identifier = "Apache-2.0"
24 |
25 | [[annotations]]
26 | path = ".github/workflows/**"
27 | SPDX-FileCopyrightText = "2025 Industria Textil de Diseño, S.A."
28 | SPDX-License-Identifier = "Apache-2.0"
29 |
30 | [[annotations]]
31 | path = "README.md"
32 | SPDX-FileCopyrightText = "2025 Industria Textil de Diseño, S.A."
33 | SPDX-License-Identifier = "Apache-2.0"
34 |
35 | [[annotations]]
36 | path = "CHANGELOG.md"
37 | SPDX-FileCopyrightText = "2025 Industria Textil de Diseño, S.A."
38 | SPDX-License-Identifier = "Apache-2.0"
39 |
40 | [[annotations]]
41 | path = "demo/demo.gif"
42 | SPDX-FileCopyrightText = "2025 Industria Textil de Diseño, S.A."
43 | SPDX-License-Identifier = "Apache-2.0"
44 |
45 | [[annotations]]
46 | path = "src/**/__tests__/*"
47 | SPDX-FileCopyrightText = "2025 Industria Textil de Diseño, S.A."
48 | SPDX-License-Identifier = "Apache-2.0"
49 |
```
--------------------------------------------------------------------------------
/src/parser/commands/BaseCommandDefinition.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { ParseResult } from '../interfaces/IParser.js';
5 |
6 | export interface CommandDefinition {
7 | command: string;
8 | patterns: RegExp[];
9 | description: string;
10 | requiredParameters: string[];
11 | optionalParameters: string[];
12 | examples: string[];
13 | parameterExtractors: Record<string, (match: RegExpMatchArray) => any>;
14 | }
15 |
16 | export abstract class BaseCommandDefinition {
17 | protected abstract definitions: CommandDefinition[];
18 |
19 | getDefinitions(): CommandDefinition[] {
20 | return this.definitions;
21 | }
22 |
23 | parseCommand(text: string): ParseResult | null {
24 | const normalizedText = text.trim().toLowerCase();
25 |
26 | for (const definition of this.definitions) {
27 | for (const pattern of definition.patterns) {
28 | const match = normalizedText.match(pattern);
29 | if (match) {
30 | const parameters: Record<string, any> = {};
31 | for (const [paramName, extractor] of Object.entries(definition.parameterExtractors)) {
32 | const value = extractor(match);
33 | if (value !== undefined) {
34 | parameters[paramName] = value;
35 | }
36 | }
37 |
38 | return {
39 | command: definition.command,
40 | parameters,
41 | confidence: 0.9,
42 | originalText: text
43 | };
44 | }
45 | }
46 | }
47 |
48 | return null;
49 | }
50 | }
51 |
```
--------------------------------------------------------------------------------
/src/parser/commands/AccessibilityCommands.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { BaseCommandDefinition, CommandDefinition } from './BaseCommandDefinition.js';
5 |
6 | export class AccessibilityCommands extends BaseCommandDefinition {
7 | protected definitions: CommandDefinition[] = [
8 | {
9 | command: 'describe elements',
10 | patterns: [
11 | /describir\s+(todos\s+los\s+)?elementos/i,
12 | /describe\s+all\s+elements/i,
13 | /mostrar\s+elementos\s+de\s+accesibilidad/i
14 | ],
15 | description: 'Describes all accessibility elements on the screen',
16 | requiredParameters: [],
17 | optionalParameters: ['sessionId'],
18 | examples: [
19 | 'describir todos los elementos',
20 | 'describe all elements',
21 | 'mostrar elementos de accesibilidad'
22 | ],
23 | parameterExtractors: {}
24 | },
25 | {
26 | command: 'describe point',
27 | patterns: [
28 | /describir\s+punto\s+(?<x>\d+)\s*,\s*(?<y>\d+)/i,
29 | /describe\s+point\s+(?<x>\d+)\s*,\s*(?<y>\d+)/i,
30 | /qué\s+hay\s+en\s+(?<x>\d+)\s*,\s*(?<y>\d+)/i
31 | ],
32 | description: 'Describes the accessibility element at a specific point',
33 | requiredParameters: ['x', 'y'],
34 | optionalParameters: ['sessionId'],
35 | examples: [
36 | 'describir punto 100, 200',
37 | 'describe point 150, 300',
38 | 'qué hay en 200, 400'
39 | ],
40 | parameterExtractors: {
41 | x: (match) => parseInt(match.groups?.x || '0', 10),
42 | y: (match) => parseInt(match.groups?.y || '0', 10)
43 | }
44 | }
45 | ];
46 | }
47 |
```
--------------------------------------------------------------------------------
/.github/workflows/sonar-branch-analysis.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: sonar-branch-analysis
2 |
3 | on:
4 | pull_request:
5 | types: [closed]
6 | branches: ["main"]
7 |
8 | env:
9 | NODE_VERSION: '20.x'
10 | PYTHON_VERSION: '3.11'
11 |
12 | jobs:
13 | verify:
14 | if: github.event.pull_request.merged == true
15 | name: Verify
16 | runs-on: macos-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Setup Python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: ${{ env.PYTHON_VERSION }}
27 |
28 | - name: Create and activate Python virtual environment
29 | run: |
30 | python -m venv venv
31 | source venv/bin/activate
32 | python -m pip install --upgrade pip
33 | echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV
34 | echo "PATH=$VIRTUAL_ENV/bin:$PATH" >> $GITHUB_ENV
35 |
36 | - name: Setup Node.js
37 | uses: actions/setup-node@v4
38 | with:
39 | node-version: ${{ env.NODE_VERSION }}
40 | cache: 'npm'
41 |
42 | - name: Install dependencies
43 | run: |
44 | source venv/bin/activate
45 | npm ci
46 |
47 | - name: Build
48 | run: npm run build
49 |
50 | - name: Run tests
51 | run: npm test
52 |
53 | - name: SonarCloud Scan
54 | uses: sonarsource/sonarcloud-github-action@master
55 | env:
56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
58 | with:
59 | args: >
60 | -Dsonar.branch.name=main
61 | -Dsonar.projectKey=mcp-server-simulator-ios-idb
62 | -Dsonar.organization=com.inditex
63 | -Dsonar.sources=src
64 | -Dsonar.tests=src/**/__tests__
65 | -Dsonar.typescript.lcov.reportPaths=coverage/lcov.info
66 | -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
67 |
```
--------------------------------------------------------------------------------
/src/parser/interfaces/IParser.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /**
5 | * IParser - Interface for natural language parser
6 | *
7 | * This interface defines the necessary methods to interpret natural
8 | * language instructions and convert them into structured commands.
9 | */
10 |
11 | export interface ParseResult {
12 | command: string;
13 | parameters: Record<string, any>;
14 | confidence: number;
15 | originalText: string;
16 | }
17 |
18 | export interface ValidationResult {
19 | isValid: boolean;
20 | missingParameters?: string[];
21 | invalidParameters?: Record<string, string>;
22 | suggestions?: string[];
23 | errorMessage?: string;
24 | }
25 |
26 | export interface IParser {
27 | /**
28 | * Parses a natural language instruction into a command structure
29 | * @param text Natural language instruction text
30 | * @returns Parsing result with extracted command and parameters
31 | */
32 | parseInstruction(text: string): Promise<ParseResult>;
33 |
34 | /**
35 | * Validates if an instruction has all required parameters
36 | * @param parseResult Parsing result to validate
37 | * @returns Validation result
38 | */
39 | validateInstruction(parseResult: ParseResult): Promise<ValidationResult>;
40 |
41 | /**
42 | * Normalizes parameters of a parsed instruction
43 | * @param parseResult Parsing result to normalize
44 | * @returns Parsing result with normalized parameters
45 | */
46 | normalizeParameters(parseResult: ParseResult): Promise<ParseResult>;
47 |
48 | /**
49 | * Generates suggestions to complete an incomplete instruction
50 | * @param partialText Partial instruction text
51 | * @returns List of suggestions to complete the instruction
52 | */
53 | suggestCompletions(partialText: string): Promise<string[]>;
54 |
55 | /**
56 | * Gets the list of commands supported by the parser
57 | * @returns List of supported commands with their descriptions
58 | */
59 | getSupportedCommands(): Promise<Array<{
60 | command: string;
61 | description: string;
62 | requiredParameters: string[];
63 | optionalParameters: string[];
64 | }>>;
65 | }
66 |
```
--------------------------------------------------------------------------------
/src/parser/commands/CommandRegistry.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { ParseResult } from '../interfaces/IParser.js';
5 | import { BaseCommandDefinition } from './BaseCommandDefinition.js';
6 |
7 | export class CommandRegistry {
8 | private commandHandlers: BaseCommandDefinition[] = [];
9 |
10 | registerHandler(handler: BaseCommandDefinition) {
11 | this.commandHandlers.push(handler);
12 | }
13 |
14 | parseInstruction(text: string): ParseResult {
15 | for (const handler of this.commandHandlers) {
16 | const result = handler.parseCommand(text);
17 | if (result) {
18 | return result;
19 | }
20 | }
21 |
22 | throw new Error(`Could not understand the instruction: ${text}`);
23 | }
24 |
25 | async getSupportedCommands(): Promise<Array<{
26 | command: string;
27 | description: string;
28 | requiredParameters: string[];
29 | optionalParameters: string[];
30 | }>> {
31 | return this.commandHandlers
32 | .flatMap(handler => handler.getDefinitions())
33 | .map(definition => ({
34 | command: definition.command,
35 | description: definition.description,
36 | requiredParameters: definition.requiredParameters,
37 | optionalParameters: definition.optionalParameters
38 | }));
39 | }
40 |
41 | async suggestCompletions(partialText: string): Promise<string[]> {
42 | const normalizedPartial = partialText.trim().toLowerCase();
43 |
44 | if (!normalizedPartial) {
45 | return [
46 | 'create session',
47 | 'list simulators',
48 | 'install app',
49 | 'launch app',
50 | 'terminate session'
51 | ];
52 | }
53 |
54 | const suggestions = new Set<string>();
55 |
56 | for (const handler of this.commandHandlers) {
57 | for (const definition of handler.getDefinitions()) {
58 | if (definition.command.toLowerCase().includes(normalizedPartial)) {
59 | suggestions.add(definition.command);
60 | }
61 |
62 | for (const example of definition.examples) {
63 | if (example.toLowerCase().includes(normalizedPartial)) {
64 | suggestions.add(example);
65 | }
66 | }
67 | }
68 | }
69 |
70 | return Array.from(suggestions).slice(0, 5);
71 | }
72 | }
73 |
```
--------------------------------------------------------------------------------
/.github/workflows/PR-verify.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: PR-verify
2 |
3 | on:
4 | pull_request:
5 |
6 | env:
7 | NODE_VERSION: '20.x'
8 | PYTHON_VERSION: '3.11'
9 |
10 | jobs:
11 | verify:
12 | name: Verify
13 | runs-on: macos-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | ref: ${{ github.event.pull_request.head.sha }}
20 |
21 | - name: Setup Python
22 | uses: actions/setup-python@v5
23 | with:
24 | python-version: ${{ env.PYTHON_VERSION }}
25 |
26 | - name: Create and activate Python virtual environment
27 | run: |
28 | python -m venv venv
29 | source venv/bin/activate
30 | python -m pip install --upgrade pip
31 | echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV
32 | echo "PATH=$VIRTUAL_ENV/bin:$PATH" >> $GITHUB_ENV
33 |
34 | - name: Setup Node.js
35 | uses: actions/setup-node@v4
36 | with:
37 | node-version: ${{ env.NODE_VERSION }}
38 | cache: 'npm'
39 |
40 | - name: Install dependencies
41 | run: |
42 | source venv/bin/activate
43 | npm ci
44 |
45 | - name: Build
46 | run: npm run build
47 |
48 | - name: Run tests
49 | run: npm test
50 |
51 | - name: SonarCloud Scan
52 | if: ${{ vars.IS_INDITEXTECH_REPO == 'true' }}
53 | uses: sonarsource/sonarcloud-github-action@master
54 | env:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
57 | with:
58 | args: >
59 | -Dsonar.projectKey=mcp-server-simulator-ios-idb
60 | -Dsonar.organization=com.inditex
61 | -Dsonar.sources=src
62 | -Dsonar.tests=src/**/__tests__
63 | -Dsonar.typescript.lcov.reportPaths=coverage/lcov.info
64 | -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
65 |
66 | repo-linter:
67 | name: Repo Linter
68 | runs-on: ubuntu-latest
69 | steps:
70 | - name: Checkout
71 | uses: actions/checkout@v4
72 | with:
73 | fetch-depth: 0
74 | ref: ${{ github.event.pull_request.head.sha }}
75 |
76 | - name: Setup Node.js
77 | uses: actions/setup-node@v4
78 | with:
79 | node-version: ${{ env.NODE_VERSION }}
80 |
81 | - name: Execute repolinter
82 | run: |
83 | npm install -g [email protected]
84 | repolinter lint .
85 |
86 | reuse-compliance:
87 | name: REUSE Compliance
88 | runs-on: ubuntu-latest
89 | steps:
90 | - name: Checkout
91 | uses: actions/checkout@v4
92 | with:
93 | fetch-depth: 0
94 | ref: ${{ github.event.pull_request.head.sha }}
95 |
96 | - name: REUSE Compliance Check
97 | uses: fsfe/reuse-action@v5
98 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | // IMMEDIATELY disable all console output to avoid interfering with stdio transport
6 | // This must come before any other imports to ensure nothing writes to stdout
7 | console.log = () => {};
8 | console.info = () => {};
9 | console.warn = () => {};
10 | console.error = (msg) => {
11 | // Only log errors to stderr, not stdout
12 | process.stderr.write(`${msg}\n`);
13 | };
14 | console.debug = () => {};
15 |
16 | import fs from 'fs';
17 | import path from 'path';
18 | import { fileURLToPath } from 'url';
19 | import mcpServer from './mcp/mcp-server.js';
20 |
21 | /**
22 | * MCP Server for iOS simulator
23 | *
24 | * This file exports all the components needed to use the MCP Server
25 | * for iOS simulator automation through idb.
26 | *
27 | * It also serves as the entry point for starting the MCP server.
28 | */
29 |
30 | // Export interfaces
31 | export { IIDBManager, SimulatorInfo, AppInfo, SessionConfig } from './idb/interfaces/IIDBManager.js';
32 | export { IParser, ParseResult, ValidationResult } from './parser/interfaces/IParser.js';
33 | export {
34 | IOrchestratorCommand,
35 | CommandType,
36 | CommandResult,
37 | CommandContext,
38 | SequenceCommand,
39 | ConditionalCommand,
40 | CommandFactory
41 | } from './orchestrator/interfaces/IOrchestratorCommand.js';
42 |
43 | // Export implementations
44 | export { IDBManager } from './idb/IDBManager.js';
45 | export { NLParser } from './parser/NLParser.js';
46 | export { MCPOrchestrator } from './orchestrator/MCPOrchestrator.js';
47 |
48 | // Export adapters
49 | export { ParserToOrchestrator } from './adapters/ParserToOrchestrator.js';
50 | export { OrchestratorToIDB } from './adapters/OrchestratorToIDB.js';
51 |
52 | /**
53 | * Main entry point for the MCP server
54 | *
55 | * This code runs when the server is started directly.
56 | * It sets up the environment and launches the MCP server.
57 | */
58 |
59 | // Create logs directory with error handling
60 | try {
61 | // Get the directory name using ESM approach
62 | const __filename = fileURLToPath(import.meta.url);
63 | const __dirname = path.dirname(__filename);
64 | // Go up one level from the src directory to the project root
65 | const logsDir = path.join(__dirname, '..', 'logs');
66 |
67 | if (!fs.existsSync(logsDir)) {
68 | fs.mkdirSync(logsDir, { recursive: true });
69 | }
70 | } catch (err) {
71 | process.stderr.write(`Error creating logs directory: ${err}\n`);
72 | }
73 |
74 | async function start() {
75 | try {
76 | // Start MCP server
77 | await mcpServer.start();
78 | } catch (error) {
79 | process.stderr.write(`Error starting MCP server: ${error}\n`);
80 | process.exit(1);
81 | }
82 | }
83 |
84 | // Handle termination signals
85 | process.on('SIGINT', () => {
86 | process.exit(0);
87 | });
88 |
89 | process.on('SIGTERM', () => {
90 | process.exit(0);
91 | });
92 |
93 | // Handle uncaught exceptions
94 | process.on('uncaughtException', (error) => {
95 | process.stderr.write(`Uncaught exception: ${error.stack}\n`);
96 | process.exit(1);
97 | });
98 |
99 | process.on('unhandledRejection', (reason) => {
100 | process.stderr.write(`Unhandled rejection: ${reason}\n`);
101 | process.exit(1);
102 | });
103 |
104 | // Start the server
105 | start();
106 |
```
--------------------------------------------------------------------------------
/src/orchestrator/interfaces/IOrchestratorCommand.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /**
5 | * IOrchestratorCommand - Interface for orchestrator commands
6 | *
7 | * This interface defines the structure of commands that the orchestrator
8 | * can handle and execute through the IDBManager.
9 | */
10 |
11 | export enum CommandType {
12 | // Simulator management commands
13 | CREATE_SIMULATOR_SESSION = 'createSimulatorSession',
14 | TERMINATE_SIMULATOR_SESSION = 'terminateSimulatorSession',
15 | LIST_AVAILABLE_SIMULATORS = 'listAvailableSimulators',
16 | LIST_BOOTED_SIMULATORS = 'listBootedSimulators',
17 | BOOT_SIMULATOR = 'bootSimulator',
18 | SHUTDOWN_SIMULATOR = 'shutdownSimulator',
19 |
20 | // Application management commands
21 | INSTALL_APP = 'installApp',
22 | LAUNCH_APP = 'launchApp',
23 | TERMINATE_APP = 'terminateApp',
24 |
25 | // UI interaction commands
26 | TAP = 'tap',
27 | SWIPE = 'swipe',
28 |
29 | // Capture and logging commands
30 | TAKE_SCREENSHOT = 'takeScreenshot',
31 | GET_SYSTEM_LOGS = 'getSystemLogs',
32 | GET_APP_LOGS = 'getAppLogs',
33 |
34 | // Verification commands
35 | IS_SIMULATOR_BOOTED = 'isSimulatorBooted',
36 | IS_APP_INSTALLED = 'isAppInstalled',
37 |
38 | // Composite commands
39 | SEQUENCE = 'sequence',
40 | CONDITIONAL = 'conditional'
41 | }
42 |
43 | export interface CommandResult {
44 | success: boolean;
45 | data?: any;
46 | error?: string;
47 | timestamp: number;
48 | }
49 |
50 | export interface CommandContext {
51 | sessionId?: string;
52 | previousResults?: Record<string, CommandResult>;
53 | variables?: Record<string, any>;
54 | }
55 |
56 | export interface IOrchestratorCommand {
57 | /**
58 | * Command type
59 | */
60 | type: CommandType;
61 |
62 | /**
63 | * Command-specific parameters
64 | */
65 | parameters: Record<string, any>;
66 |
67 | /**
68 | * Unique command ID
69 | */
70 | id: string;
71 |
72 | /**
73 | * Human-readable command description
74 | */
75 | description?: string;
76 |
77 | /**
78 | * Maximum execution time in milliseconds
79 | */
80 | timeout?: number;
81 |
82 | /**
83 | * Number of retries in case of failure
84 | */
85 | retries?: number;
86 |
87 | /**
88 | * Parameter validation function
89 | */
90 | validate?: (context: CommandContext) => Promise<boolean>;
91 |
92 | /**
93 | * Parameter transformation function before execution
94 | */
95 | transformParameters?: (context: CommandContext) => Promise<Record<string, any>>;
96 |
97 | /**
98 | * Custom error handling function
99 | */
100 | onError?: (error: Error, context: CommandContext) => Promise<CommandResult>;
101 | }
102 |
103 | /**
104 | * Sequence command that executes multiple commands in order
105 | */
106 | export interface SequenceCommand extends IOrchestratorCommand {
107 | type: CommandType.SEQUENCE;
108 | parameters: {
109 | commands: IOrchestratorCommand[];
110 | stopOnError?: boolean;
111 | };
112 | }
113 |
114 | /**
115 | * Conditional command that executes one command or another based on a condition
116 | */
117 | export interface ConditionalCommand extends IOrchestratorCommand {
118 | type: CommandType.CONDITIONAL;
119 | parameters: {
120 | condition: (context: CommandContext) => Promise<boolean>;
121 | ifTrue: IOrchestratorCommand;
122 | ifFalse?: IOrchestratorCommand;
123 | };
124 | }
125 |
126 | /**
127 | * Command factory to create IOrchestratorCommand instances
128 | */
129 | export interface CommandFactory {
130 | createCommand(type: CommandType, parameters: Record<string, any>, description?: string): IOrchestratorCommand;
131 | createSequence(commands: IOrchestratorCommand[], stopOnError?: boolean): SequenceCommand;
132 | createConditional(
133 | condition: (context: CommandContext) => Promise<boolean>,
134 | ifTrue: IOrchestratorCommand,
135 | ifFalse?: IOrchestratorCommand
136 | ): ConditionalCommand;
137 | }
138 |
```
--------------------------------------------------------------------------------
/src/parser/commands/CaptureCommands.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { BaseCommandDefinition, CommandDefinition } from './BaseCommandDefinition.js';
5 |
6 | export class CaptureCommands extends BaseCommandDefinition {
7 | protected definitions: CommandDefinition[] = [
8 | {
9 | command: 'capture screen',
10 | patterns: [
11 | /capturar\s+(la\s+)?pantalla(\s+en\s+(?<outputPath>[^\s,]+))?/i,
12 | /screenshot(\s+en\s+(?<outputPath>[^\s,]+))?/i,
13 | /tomar\s+(una\s+)?captura(\s+en\s+(?<outputPath>[^\s,]+))?/i,
14 | /capture\s+(the\s+)?screen(\s+to\s+(?<outputPath>[^\s,]+))?/i,
15 | /take\s+(a\s+)?screenshot(\s+to\s+(?<outputPath>[^\s,]+))?/i
16 | ],
17 | description: 'Captures a screenshot of the simulator',
18 | requiredParameters: [],
19 | optionalParameters: ['outputPath', 'sessionId'],
20 | examples: [
21 | 'capturar pantalla',
22 | 'screenshot en /ruta/captura.png',
23 | 'tomar una captura',
24 | 'capture screen',
25 | 'take screenshot to /path/capture.png'
26 | ],
27 | parameterExtractors: {
28 | outputPath: (match) => match.groups?.outputPath?.trim()
29 | }
30 | },
31 | {
32 | command: 'record video',
33 | patterns: [
34 | /grabar\s+video\s+(?<outputPath>[^\s,]+)/i,
35 | /record\s+video\s+(?<outputPath>[^\s,]+)/i,
36 | /iniciar\s+grabación\s+de\s+video\s+(?<outputPath>[^\s,]+)/i,
37 | /start\s+recording\s+video\s+(?<outputPath>[^\s,]+)/i,
38 | /begin\s+video\s+recording\s+(?<outputPath>[^\s,]+)/i
39 | ],
40 | description: 'Starts video recording of the simulator',
41 | requiredParameters: ['outputPath'],
42 | optionalParameters: ['sessionId'],
43 | examples: [
44 | 'grabar video /ruta/video.mp4',
45 | 'record video /tmp/captura.mp4',
46 | 'iniciar grabación de video /ruta/salida.mp4',
47 | 'record video /path/video.mp4',
48 | 'start recording video /path/output.mp4'
49 | ],
50 | parameterExtractors: {
51 | outputPath: (match) => match.groups?.outputPath?.trim()
52 | }
53 | },
54 | {
55 | command: 'stop recording',
56 | patterns: [
57 | /detener\s+grabación(\s+de\s+video)?/i,
58 | /parar\s+grabación(\s+de\s+video)?/i,
59 | /stop\s+recording/i,
60 | /end\s+recording/i,
61 | /stop\s+video\s+recording/i
62 | ],
63 | description: 'Stops video recording of the simulator',
64 | requiredParameters: [],
65 | optionalParameters: ['recordingId', 'sessionId'],
66 | examples: [
67 | 'detener grabación',
68 | 'parar grabación de video',
69 | 'stop recording',
70 | 'end recording',
71 | 'stop video recording'
72 | ],
73 | parameterExtractors: {}
74 | },
75 | {
76 | command: 'get logs',
77 | patterns: [
78 | /obtener\s+logs(\s+de\s+(?<bundleId>[^\s,]+))?/i,
79 | /mostrar\s+logs(\s+de\s+(?<bundleId>[^\s,]+))?/i,
80 | /get\s+logs(\s+for\s+(?<bundleId>[^\s,]+))?/i,
81 | /show\s+logs(\s+for\s+(?<bundleId>[^\s,]+))?/i,
82 | /display\s+logs(\s+for\s+(?<bundleId>[^\s,]+))?/i
83 | ],
84 | description: 'Gets system logs or logs from a specific application',
85 | requiredParameters: [],
86 | optionalParameters: ['bundleId', 'limit', 'sessionId'],
87 | examples: [
88 | 'obtener logs',
89 | 'mostrar logs de com.example.app',
90 | 'get logs for com.apple.mobilesafari',
91 | 'show logs',
92 | 'display logs for com.example.app'
93 | ],
94 | parameterExtractors: {
95 | bundleId: (match) => match.groups?.bundleId?.trim()
96 | }
97 | }
98 | ];
99 | }
100 |
```
--------------------------------------------------------------------------------
/src/parser/commands/DebugCommands.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { BaseCommandDefinition, CommandDefinition } from './BaseCommandDefinition.js';
5 |
6 | export class DebugCommands extends BaseCommandDefinition {
7 | protected definitions: CommandDefinition[] = [
8 | {
9 | command: 'start debug',
10 | patterns: [
11 | /iniciar\s+debug\s+(?<bundleId>[^\s,]+)/i,
12 | /start\s+debug\s+(?<bundleId>[^\s,]+)/i,
13 | /debug\s+app\s+(?<bundleId>[^\s,]+)/i,
14 | /begin\s+debug\s+(?<bundleId>[^\s,]+)/i,
15 | /launch\s+debug\s+(?<bundleId>[^\s,]+)/i
16 | ],
17 | description: 'Starts a debug session for an application',
18 | requiredParameters: ['bundleId'],
19 | optionalParameters: ['sessionId'],
20 | examples: [
21 | 'iniciar debug com.example.app',
22 | 'start debug com.apple.mobilesafari',
23 | 'debug app com.example.app',
24 | 'begin debug com.example.app',
25 | 'launch debug com.apple.mobilesafari'
26 | ],
27 | parameterExtractors: {
28 | bundleId: (match) => match.groups?.bundleId?.trim()
29 | }
30 | },
31 | {
32 | command: 'stop debug',
33 | patterns: [
34 | /detener\s+debug/i,
35 | /parar\s+debug/i,
36 | /stop\s+debug/i,
37 | /end\s+debug/i,
38 | /terminate\s+debug/i
39 | ],
40 | description: 'Stops a debug session',
41 | requiredParameters: [],
42 | optionalParameters: ['sessionId'],
43 | examples: [
44 | 'detener debug',
45 | 'parar debug',
46 | 'stop debug',
47 | 'end debug',
48 | 'terminate debug'
49 | ],
50 | parameterExtractors: {}
51 | },
52 | {
53 | command: 'debug status',
54 | patterns: [
55 | /estado\s+debug/i,
56 | /status\s+debug/i,
57 | /información\s+debug/i,
58 | /debug\s+status/i,
59 | /get\s+debug\s+status/i
60 | ],
61 | description: 'Gets the debug session status',
62 | requiredParameters: [],
63 | optionalParameters: ['sessionId'],
64 | examples: [
65 | 'estado debug',
66 | 'status debug',
67 | 'información debug',
68 | 'debug status',
69 | 'get debug status'
70 | ],
71 | parameterExtractors: {}
72 | },
73 | {
74 | command: 'list crash logs',
75 | patterns: [
76 | /listar\s+crash\s+logs/i,
77 | /mostrar\s+crash\s+logs/i,
78 | /list\s+crash\s+logs/i,
79 | /show\s+crash\s+logs/i,
80 | /display\s+crash\s+logs/i
81 | ],
82 | description: 'Lists available crash logs',
83 | requiredParameters: [],
84 | optionalParameters: ['bundleId', 'sessionId'],
85 | examples: [
86 | 'listar crash logs',
87 | 'mostrar crash logs',
88 | 'list crash logs',
89 | 'show crash logs',
90 | 'display crash logs'
91 | ],
92 | parameterExtractors: {}
93 | },
94 | {
95 | command: 'show crash log',
96 | patterns: [
97 | /mostrar\s+crash\s+log\s+(?<crashName>[^\s,]+)/i,
98 | /ver\s+crash\s+log\s+(?<crashName>[^\s,]+)/i,
99 | /show\s+crash\s+log\s+(?<crashName>[^\s,]+)/i,
100 | /display\s+crash\s+log\s+(?<crashName>[^\s,]+)/i,
101 | /view\s+crash\s+log\s+(?<crashName>[^\s,]+)/i
102 | ],
103 | description: 'Gets the content of a crash log',
104 | requiredParameters: ['crashName'],
105 | optionalParameters: ['sessionId'],
106 | examples: [
107 | 'mostrar crash log crash_2023-01-01',
108 | 'ver crash log app_crash_123',
109 | 'show crash log system_crash',
110 | 'display crash log error_log_123',
111 | 'view crash log app_crash_456'
112 | ],
113 | parameterExtractors: {
114 | crashName: (match) => match.groups?.crashName?.trim()
115 | }
116 | },
117 | {
118 | command: 'delete crash logs',
119 | patterns: [
120 | /eliminar\s+crash\s+logs/i,
121 | /borrar\s+crash\s+logs/i,
122 | /delete\s+crash\s+logs/i,
123 | /remove\s+crash\s+logs/i,
124 | /clear\s+crash\s+logs/i
125 | ],
126 | description: 'Deletes crash logs',
127 | requiredParameters: [],
128 | optionalParameters: ['bundleId', 'sessionId', 'all'],
129 | examples: [
130 | 'eliminar crash logs',
131 | 'borrar crash logs',
132 | 'delete crash logs',
133 | 'remove crash logs',
134 | 'clear crash logs'
135 | ],
136 | parameterExtractors: {}
137 | }
138 | ];
139 | }
140 |
```
--------------------------------------------------------------------------------
/src/parser/commands/AppCommands.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { BaseCommandDefinition, CommandDefinition } from './BaseCommandDefinition.js';
5 |
6 | export class AppCommands extends BaseCommandDefinition {
7 | protected definitions: CommandDefinition[] = [
8 | {
9 | command: 'install app',
10 | patterns: [
11 | /instalar\s+(la\s+)?app(\s+en\s+la\s+ruta)?\s+(?<appPath>[^\s,]+)/i,
12 | /instalar\s+(la\s+)?aplicación(\s+en\s+la\s+ruta)?\s+(?<appPath>[^\s,]+)/i,
13 | /install\s+(the\s+)?app(\s+at)?\s+(?<appPath>[^\s,]+)/i,
14 | /install\s+(the\s+)?application(\s+at)?\s+(?<appPath>[^\s,]+)/i
15 | ],
16 | description: 'Installs an application on the simulator',
17 | requiredParameters: ['appPath'],
18 | optionalParameters: ['sessionId'],
19 | examples: [
20 | 'instalar app /ruta/a/la/app.ipa',
21 | 'instalar la aplicación /ruta/a/la/app.app',
22 | 'install app /path/to/app.ipa',
23 | 'install application /path/to/app.app'
24 | ],
25 | parameterExtractors: {
26 | appPath: (match) => match.groups?.appPath?.trim()
27 | }
28 | },
29 | {
30 | command: 'launch app',
31 | patterns: [
32 | /lanzar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
33 | /abrir\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
34 | /iniciar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
35 | /launch\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i,
36 | /open\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i,
37 | /start\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i
38 | ],
39 | description: 'Launches an application on the simulator',
40 | requiredParameters: ['bundleId'],
41 | optionalParameters: ['sessionId'],
42 | examples: [
43 | 'lanzar app com.example.app',
44 | 'abrir app com.apple.mobilesafari',
45 | 'launch app com.example.app',
46 | 'open app com.apple.mobilesafari'
47 | ],
48 | parameterExtractors: {
49 | bundleId: (match) => match.groups?.bundleId?.trim()
50 | }
51 | },
52 | {
53 | command: 'terminate app',
54 | patterns: [
55 | /terminar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
56 | /cerrar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
57 | /matar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
58 | /terminate\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i,
59 | /close\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i,
60 | /kill\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i
61 | ],
62 | description: 'Terminates a running application',
63 | requiredParameters: ['bundleId'],
64 | optionalParameters: ['sessionId'],
65 | examples: [
66 | 'terminar app com.example.app',
67 | 'cerrar app com.apple.mobilesafari',
68 | 'matar app com.example.app',
69 | 'terminate app com.example.app',
70 | 'close app com.apple.mobilesafari',
71 | 'kill app com.example.app'
72 | ],
73 | parameterExtractors: {
74 | bundleId: (match) => match.groups?.bundleId?.trim()
75 | }
76 | },
77 | {
78 | command: 'uninstall app',
79 | patterns: [
80 | /desinstalar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
81 | /eliminar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
82 | /borrar\s+(la\s+)?app\s+(?<bundleId>[^\s,]+)/i,
83 | /uninstall\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i,
84 | /remove\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i,
85 | /delete\s+(the\s+)?app\s+(?<bundleId>[^\s,]+)/i
86 | ],
87 | description: 'Uninstalls an application',
88 | requiredParameters: ['bundleId'],
89 | optionalParameters: ['sessionId'],
90 | examples: [
91 | 'desinstalar app com.example.app',
92 | 'eliminar app com.apple.mobilesafari',
93 | 'borrar app com.example.app',
94 | 'uninstall app com.example.app',
95 | 'remove app com.apple.mobilesafari',
96 | 'delete app com.example.app'
97 | ],
98 | parameterExtractors: {
99 | bundleId: (match) => match.groups?.bundleId?.trim()
100 | }
101 | },
102 | {
103 | command: 'list apps',
104 | patterns: [
105 | /listar\s+(las\s+)?apps/i,
106 | /mostrar\s+(las\s+)?apps/i,
107 | /qué\s+apps\s+(hay|están\s+instaladas)/i,
108 | /list\s+(the\s+)?apps/i,
109 | /show\s+(the\s+)?apps/i,
110 | /what\s+apps\s+(are\s+there|are\s+installed)/i
111 | ],
112 | description: 'Lists installed applications',
113 | requiredParameters: [],
114 | optionalParameters: ['sessionId'],
115 | examples: [
116 | 'listar apps',
117 | 'mostrar apps',
118 | 'qué apps hay',
119 | 'list apps',
120 | 'show apps',
121 | 'what apps are installed'
122 | ],
123 | parameterExtractors: {}
124 | }
125 | ];
126 | }
127 |
```
--------------------------------------------------------------------------------
/src/parser/NLParser.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { IParser, ParseResult, ValidationResult } from './interfaces/IParser.js';
5 | import { CommandRegistry } from './commands/CommandRegistry.js';
6 | import { SimulatorCommands } from './commands/SimulatorCommands.js';
7 | import { AppCommands } from './commands/AppCommands.js';
8 | import { UICommands } from './commands/UICommands.js';
9 | import { AccessibilityCommands } from './commands/AccessibilityCommands.js';
10 | import { CaptureCommands } from './commands/CaptureCommands.js';
11 | import { DebugCommands } from './commands/DebugCommands.js';
12 | import { MiscCommands } from './commands/MiscCommands.js';
13 |
14 | /**
15 | * Natural language parser implementation
16 | */
17 | export class NLParser implements IParser {
18 | private commandRegistry: CommandRegistry;
19 |
20 | /**
21 | * Constructor
22 | */
23 | constructor() {
24 | this.commandRegistry = new CommandRegistry();
25 |
26 | // Register all command handlers
27 | this.commandRegistry.registerHandler(new SimulatorCommands());
28 | this.commandRegistry.registerHandler(new AppCommands());
29 | this.commandRegistry.registerHandler(new UICommands());
30 | this.commandRegistry.registerHandler(new AccessibilityCommands());
31 | this.commandRegistry.registerHandler(new CaptureCommands());
32 | this.commandRegistry.registerHandler(new DebugCommands());
33 | this.commandRegistry.registerHandler(new MiscCommands());
34 | }
35 |
36 | /**
37 | * Parses a natural language instruction into a command structure
38 | * @param text Natural language instruction text
39 | * @returns Parsing result with extracted command and parameters
40 | */
41 | async parseInstruction(text: string): Promise<ParseResult> {
42 | return this.commandRegistry.parseInstruction(text);
43 | }
44 |
45 | /**
46 | * Validates if an instruction has all required parameters
47 | * @param parseResult Parsing result to validate
48 | * @returns Validation result
49 | */
50 | async validateInstruction(parseResult: ParseResult): Promise<ValidationResult> {
51 | // Get supported command definitions
52 | const supportedCommands = await this.commandRegistry.getSupportedCommands();
53 | const definition = supportedCommands.find(cmd => cmd.command === parseResult.command);
54 |
55 | if (!definition) {
56 | return {
57 | isValid: false,
58 | errorMessage: `Unrecognized command: ${parseResult.command}`
59 | };
60 | }
61 |
62 | // Verify required parameters
63 | const missingParameters = definition.requiredParameters.filter(
64 | param => !(param in parseResult.parameters)
65 | );
66 |
67 | if (missingParameters.length > 0) {
68 | return {
69 | isValid: false,
70 | missingParameters,
71 | errorMessage: `Missing required parameters: ${missingParameters.join(', ')}`
72 | };
73 | }
74 |
75 | // Validate parameter types
76 | const invalidParameters: Record<string, string> = {};
77 | for (const [param, value] of Object.entries(parseResult.parameters)) {
78 | if (typeof value === 'undefined' || value === null) {
79 | invalidParameters[param] = 'Value cannot be null or undefined';
80 | }
81 | }
82 |
83 | if (Object.keys(invalidParameters).length > 0) {
84 | return {
85 | isValid: false,
86 | invalidParameters,
87 | errorMessage: 'Some parameters have invalid values'
88 | };
89 | }
90 |
91 | return {
92 | isValid: true
93 | };
94 | }
95 |
96 | /**
97 | * Normalizes parameters of a parsed instruction
98 | * @param parseResult Parsing result to normalize
99 | * @returns Parsing result with normalized parameters
100 | */
101 | async normalizeParameters(parseResult: ParseResult): Promise<ParseResult> {
102 | const normalizedParameters = { ...parseResult.parameters };
103 |
104 | // Normalize numeric values
105 | for (const [key, value] of Object.entries(normalizedParameters)) {
106 | if (typeof value === 'string' && !isNaN(Number(value))) {
107 | normalizedParameters[key] = Number(value);
108 | }
109 | }
110 |
111 | // Normalize booleans
112 | for (const [key, value] of Object.entries(normalizedParameters)) {
113 | if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase())) {
114 | normalizedParameters[key] = value.toLowerCase() === 'true';
115 | }
116 | }
117 |
118 | return {
119 | ...parseResult,
120 | parameters: normalizedParameters
121 | };
122 | }
123 |
124 | /**
125 | * Generates suggestions to complete an incomplete instruction
126 | * @param partialText Partial instruction text
127 | * @returns List of suggestions to complete the instruction
128 | */
129 | async suggestCompletions(partialText: string): Promise<string[]> {
130 | return this.commandRegistry.suggestCompletions(partialText);
131 | }
132 |
133 | /**
134 | * Gets the list of commands supported by the parser
135 | * @returns List of supported commands with their descriptions
136 | */
137 | async getSupportedCommands(): Promise<Array<{
138 | command: string;
139 | description: string;
140 | requiredParameters: string[];
141 | optionalParameters: string[];
142 | }>> {
143 | return this.commandRegistry.getSupportedCommands();
144 | }
145 | }
146 |
```
--------------------------------------------------------------------------------
/src/mcp/mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6 | import {
7 | CallToolRequestSchema,
8 | ErrorCode,
9 | McpError,
10 | } from '@modelcontextprotocol/sdk/types.js';
11 | import fs from 'fs';
12 | import path from 'path';
13 | import { fileURLToPath } from 'url';
14 |
15 | // Export implementations
16 | import { IDBManager } from '../idb/IDBManager.js';
17 | import { NLParser } from '../parser/NLParser.js';
18 | import { MCPOrchestrator } from '../orchestrator/MCPOrchestrator.js';
19 |
20 | // Log configuration
21 | // Get the directory name using ESM approach
22 | const __filename = fileURLToPath(import.meta.url);
23 | const __dirname = path.dirname(__filename);
24 | // Go up two levels from the src/mcp directory to the project root
25 | const logsDir = path.join(__dirname, '..', '..', 'logs');
26 | if (!fs.existsSync(logsDir)) {
27 | fs.mkdirSync(logsDir, { recursive: true });
28 | }
29 |
30 | // Function to write logs to file without using console
31 | const logToFile = (message: string, level: string = 'info') => {
32 | try {
33 | const timestamp = new Date().toISOString();
34 | const logMessage = `${timestamp} [${level.toUpperCase()}] ${message}\n`;
35 | fs.appendFileSync(path.join(logsDir, 'mcp-server.log'), logMessage);
36 | } catch (error) {
37 | // Not much we can do if logging fails
38 | }
39 | };
40 |
41 | /**
42 | * Create a complete MCP Server instance
43 | * @returns Object with all necessary instances
44 | */
45 | export function createMCPServer() {
46 | // Create instances
47 | const idbManager = new IDBManager();
48 | const parser = new NLParser();
49 | const orchestrator = new MCPOrchestrator(parser, idbManager);
50 |
51 | return {
52 | idbManager,
53 | parser,
54 | orchestrator
55 | };
56 | }
57 |
58 | /**
59 | * MCP Server implementation for iOS simulator
60 | */
61 | class MCPSimulatorServer {
62 | private server: Server;
63 | private orchestrator: ReturnType<typeof createMCPServer>['orchestrator'];
64 |
65 | constructor() {
66 | // Create component instances
67 | const { orchestrator } = createMCPServer();
68 | this.orchestrator = orchestrator;
69 |
70 | // Create MCP server
71 | this.server = new Server(
72 | {
73 | name: 'iOS Simulator MCP Server',
74 | version: '1.0.1',
75 | },
76 | {
77 | capabilities: {
78 | resources: {},
79 | tools: {},
80 | },
81 | }
82 | );
83 |
84 | // Register tools
85 | this.registerTools();
86 |
87 | // Handle errors
88 | this.server.onerror = (error) => {
89 | logToFile(`MCP server error: ${error}`, 'error');
90 | };
91 |
92 | // Handle termination signals
93 | process.on('SIGINT', async () => {
94 | await this.close();
95 | process.exit(0);
96 | });
97 |
98 | process.on('SIGTERM', async () => {
99 | await this.close();
100 | process.exit(0);
101 | });
102 | }
103 |
104 | /**
105 | * Register MCP server tools
106 | */
107 | private registerTools() {
108 | // Main tool for processing natural language instructions
109 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
110 | if (request.params.name === 'process-instruction') {
111 | const instruction = request.params.arguments?.instruction;
112 |
113 | if (!instruction || typeof instruction !== 'string') {
114 | throw new McpError(
115 | ErrorCode.InvalidParams,
116 | 'Text instruction is required'
117 | );
118 | }
119 |
120 | logToFile(`Processing instruction: ${instruction}`);
121 |
122 | try {
123 | const result = await this.orchestrator.processInstruction(instruction);
124 |
125 | return {
126 | content: [
127 | {
128 | type: 'text',
129 | text: JSON.stringify(result),
130 | },
131 | ],
132 | };
133 | } catch (error) {
134 | logToFile(`Error processing instruction: ${error}`, 'error');
135 |
136 | return {
137 | content: [
138 | {
139 | type: 'text',
140 | text: `Error: ${error instanceof Error ? error.message : String(error)}`,
141 | },
142 | ],
143 | isError: true,
144 | };
145 | }
146 | }
147 |
148 | throw new McpError(
149 | ErrorCode.MethodNotFound,
150 | `Unknown tool: ${request.params.name}`
151 | );
152 | });
153 | }
154 |
155 | /**
156 | * Start the MCP server with stdio transport
157 | */
158 | async start() {
159 | logToFile('Starting MCP server with stdio transport');
160 |
161 | try {
162 | const transport = new StdioServerTransport();
163 | await this.server.connect(transport);
164 |
165 | logToFile('MCP server started successfully');
166 | } catch (error) {
167 | logToFile(`Error starting MCP server: ${error}`, 'error');
168 | throw error;
169 | }
170 | }
171 |
172 | /**
173 | * Close the MCP server
174 | */
175 | async close() {
176 | logToFile('Closing MCP server');
177 |
178 | try {
179 | await this.server.close();
180 | logToFile('MCP server closed successfully');
181 | } catch (error) {
182 | logToFile(`Error closing MCP server: ${error}`, 'error');
183 | }
184 | }
185 | }
186 |
187 | // Export a server instance
188 | export default new MCPSimulatorServer();
189 |
```
--------------------------------------------------------------------------------
/src/parser/commands/SimulatorCommands.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { BaseCommandDefinition, CommandDefinition } from './BaseCommandDefinition.js';
5 |
6 | export class SimulatorCommands extends BaseCommandDefinition {
7 | protected definitions: CommandDefinition[] = [
8 | {
9 | command: 'create session',
10 | patterns: [
11 | /crear\s+(una\s+)?sesión(\s+de\s+simulador)?(\s+con\s+(?<deviceName>[^,]+))?/i,
12 | /iniciar\s+(un\s+)?simulador(\s+(?<deviceName>[^,]+))?/i,
13 | /create\s+(a\s+)?session(\s+with\s+(?<deviceName>[^,]+))?/i,
14 | /start\s+(a\s+)?simulator(\s+(?<deviceName>[^,]+))?/i,
15 | /launch\s+(a\s+)?simulator(\s+(?<deviceName>[^,]+))?/i
16 | ],
17 | description: 'Creates a new simulator session',
18 | requiredParameters: [],
19 | optionalParameters: ['deviceName', 'platformVersion', 'autoboot'],
20 | examples: [
21 | 'crear sesión',
22 | 'crear una sesión de simulador',
23 | 'iniciar simulador iPhone 12',
24 | 'create session with iPhone 12',
25 | 'start simulator iPhone 13'
26 | ],
27 | parameterExtractors: {
28 | deviceName: (match) => match.groups?.deviceName?.trim()
29 | }
30 | },
31 | {
32 | command: 'end session',
33 | patterns: [
34 | /terminar\s+(la\s+)?sesión/i,
35 | /cerrar\s+(el\s+)?simulador/i,
36 | /end\s+(the\s+)?session/i,
37 | /close\s+(the\s+)?simulator/i,
38 | /terminate\s+(the\s+)?session/i
39 | ],
40 | description: 'Ends the current simulator session',
41 | requiredParameters: [],
42 | optionalParameters: ['sessionId'],
43 | examples: [
44 | 'terminar sesión',
45 | 'cerrar simulador',
46 | 'end session',
47 | 'close simulator',
48 | 'terminate session'
49 | ],
50 | parameterExtractors: {}
51 | },
52 | {
53 | command: 'list simulators',
54 | patterns: [
55 | /listar\s+(los\s+)?simuladores/i,
56 | /mostrar\s+(los\s+)?simuladores/i,
57 | /qué\s+simuladores\s+(hay|están\s+disponibles)/i,
58 | /list\s+(all\s+)?simulators/i,
59 | /show\s+(all\s+)?simulators/i,
60 | /display\s+(all\s+)?simulators/i
61 | ],
62 | description: 'Lists available simulators',
63 | requiredParameters: [],
64 | optionalParameters: [],
65 | examples: [
66 | 'listar simuladores',
67 | 'mostrar simuladores',
68 | 'qué simuladores hay',
69 | 'list simulators',
70 | 'show all simulators',
71 | 'display simulators'
72 | ],
73 | parameterExtractors: {}
74 | },
75 | {
76 | command: 'list booted simulators',
77 | patterns: [
78 | /listar\s+(los\s+)?simuladores\s+arrancados/i,
79 | /mostrar\s+(los\s+)?simuladores\s+arrancados/i,
80 | /qué\s+simuladores\s+están\s+arrancados/i,
81 | /list\s+(all\s+)?booted\s+simulators/i,
82 | /show\s+running\s+simulators/i,
83 | /display\s+active\s+simulators/i
84 | ],
85 | description: 'Lists booted simulators',
86 | requiredParameters: [],
87 | optionalParameters: [],
88 | examples: [
89 | 'listar simuladores arrancados',
90 | 'mostrar simuladores arrancados',
91 | 'qué simuladores están arrancados',
92 | 'list booted simulators',
93 | 'show running simulators',
94 | 'display active simulators'
95 | ],
96 | parameterExtractors: {}
97 | },
98 | {
99 | command: 'boot simulator',
100 | patterns: [
101 | /arrancar\s+(el\s+)?simulador\s+(?<udid>[a-zA-Z0-9-]+)/i,
102 | /bootear\s+(el\s+)?simulador\s+(?<udid>[a-zA-Z0-9-]+)/i,
103 | /boot\s+(the\s+)?simulator\s+(?<udid>[a-zA-Z0-9-]+)/i,
104 | /start\s+(the\s+)?simulator\s+(?<udid>[a-zA-Z0-9-]+)/i
105 | ],
106 | description: 'Boots a simulator by its UDID',
107 | requiredParameters: ['udid'],
108 | optionalParameters: [],
109 | examples: [
110 | 'arrancar simulador 5A321B8F-4D85-4267-9F79-2F5C91D142C2',
111 | 'bootear simulador 5A321B8F-4D85-4267-9F79-2F5C91D142C2',
112 | 'boot simulator 5A321B8F-4D85-4267-9F79-2F5C91D142C2',
113 | 'start simulator 5A321B8F-4D85-4267-9F79-2F5C91D142C2'
114 | ],
115 | parameterExtractors: {
116 | udid: (match) => match.groups?.udid?.trim()
117 | }
118 | },
119 | {
120 | command: 'shutdown simulator',
121 | patterns: [
122 | /apagar\s+(el\s+)?simulador\s+(?<udid>[a-zA-Z0-9-]+)/i,
123 | /shutdown\s+(el\s+)?simulador\s+(?<udid>[a-zA-Z0-9-]+)/i,
124 | /shutdown\s+(the\s+)?simulator\s+(?<udid>[a-zA-Z0-9-]+)/i,
125 | /turn\s+off\s+(the\s+)?simulator\s+(?<udid>[a-zA-Z0-9-]+)/i
126 | ],
127 | description: 'Shuts down a simulator by its UDID',
128 | requiredParameters: ['udid'],
129 | optionalParameters: [],
130 | examples: [
131 | 'apagar simulador 5A321B8F-4D85-4267-9F79-2F5C91D142C2',
132 | 'shutdown simulador 5A321B8F-4D85-4267-9F79-2F5C91D142C2',
133 | 'shutdown simulator 5A321B8F-4D85-4267-9F79-2F5C91D142C2',
134 | 'turn off simulator 5A321B8F-4D85-4267-9F79-2F5C91D142C2'
135 | ],
136 | parameterExtractors: {
137 | udid: (match) => match.groups?.udid?.trim()
138 | }
139 | },
140 | {
141 | command: 'focus simulator',
142 | patterns: [
143 | /enfocar\s+(el\s+)?simulador/i,
144 | /focus\s+(el\s+)?simulador/i,
145 | /traer\s+(el\s+)?simulador\s+al\s+frente/i,
146 | /focus\s+(the\s+)?simulator/i,
147 | /bring\s+(the\s+)?simulator\s+to\s+front/i
148 | ],
149 | description: 'Focuses the simulator window',
150 | requiredParameters: [],
151 | optionalParameters: ['sessionId'],
152 | examples: [
153 | 'enfocar simulador',
154 | 'focus simulador',
155 | 'traer simulador al frente',
156 | 'focus simulator',
157 | 'bring simulator to front'
158 | ],
159 | parameterExtractors: {}
160 | }
161 | ];
162 | }
163 |
```
--------------------------------------------------------------------------------
/src/parser/commands/UICommands.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { BaseCommandDefinition, CommandDefinition } from './BaseCommandDefinition.js';
5 |
6 | export class UICommands extends BaseCommandDefinition {
7 | protected definitions: CommandDefinition[] = [
8 | {
9 | command: 'tap',
10 | patterns: [
11 | /tap(\s+at)?\s+(?<x>\d+)\s*,\s*(?<y>\d+)/i,
12 | /tocar(\s+en)?\s+(?<x>\d+)\s*,\s*(?<y>\d+)/i,
13 | /pulsar(\s+en)?\s+(?<x>\d+)\s*,\s*(?<y>\d+)/i
14 | ],
15 | description: 'Performs a tap at the specified coordinates',
16 | requiredParameters: ['x', 'y'],
17 | optionalParameters: ['sessionId', 'duration'],
18 | examples: [
19 | 'tap en 100, 200',
20 | 'tocar 150, 300',
21 | 'pulsar en 200, 400',
22 | 'tap at 100, 200',
23 | 'tap 150, 300'
24 | ],
25 | parameterExtractors: {
26 | x: (match) => parseInt(match.groups?.x || '0', 10),
27 | y: (match) => parseInt(match.groups?.y || '0', 10)
28 | }
29 | },
30 | {
31 | command: 'swipe',
32 | patterns: [
33 | /swipe\s+from\s+(?<startX>\d+)\s*,\s*(?<startY>\d+)\s+to\s+(?<endX>\d+)\s*,\s*(?<endY>\d+)(\s+with\s+duration\s+(?<duration>\d+))?/i,
34 | /deslizar\s+desde\s+(?<startX>\d+)\s*,\s*(?<startY>\d+)\s+hasta\s+(?<endX>\d+)\s*,\s*(?<endY>\d+)(\s+con\s+duración\s+(?<duration>\d+))?/i
35 | ],
36 | description: 'Performs a swipe from one point to another',
37 | requiredParameters: ['startX', 'startY', 'endX', 'endY'],
38 | optionalParameters: ['duration', 'delta', 'sessionId'],
39 | examples: [
40 | 'swipe desde 100, 200 hasta 300, 400',
41 | 'deslizar desde 150, 300 hasta 150, 100 con duración 500',
42 | 'swipe from 100, 200 to 300, 400',
43 | 'swipe from 150, 300 to 150, 100 with duration 500'
44 | ],
45 | parameterExtractors: {
46 | startX: (match) => parseInt(match.groups?.startX || '0', 10),
47 | startY: (match) => parseInt(match.groups?.startY || '0', 10),
48 | endX: (match) => parseInt(match.groups?.endX || '0', 10),
49 | endY: (match) => parseInt(match.groups?.endY || '0', 10),
50 | duration: (match) => match.groups?.duration ? parseInt(match.groups.duration, 10) : undefined
51 | }
52 | },
53 | {
54 | command: 'press device button',
55 | patterns: [
56 | /presionar\s+(el\s+)?botón\s+del\s+dispositivo\s+(?<button>APPLE_PAY|HOME|LOCK|SIDE_BUTTON|SIRI)/i,
57 | /pulsar\s+(el\s+)?botón\s+del\s+dispositivo\s+(?<button>APPLE_PAY|HOME|LOCK|SIDE_BUTTON|SIRI)/i,
58 | /presionar\s+(el\s+)?botón\s+físico\s+(?<button>APPLE_PAY|HOME|LOCK|SIDE_BUTTON|SIRI)/i,
59 | /press\s+(the\s+)?device\s+button\s+(?<button>APPLE_PAY|HOME|LOCK|SIDE_BUTTON|SIRI)/i,
60 | /push\s+(the\s+)?device\s+button\s+(?<button>APPLE_PAY|HOME|LOCK|SIDE_BUTTON|SIRI)/i,
61 | /tap\s+(the\s+)?device\s+button\s+(?<button>APPLE_PAY|HOME|LOCK|SIDE_BUTTON|SIRI)/i
62 | ],
63 | description: 'Presses a hardware device button',
64 | requiredParameters: ['button'],
65 | optionalParameters: ['duration', 'sessionId'],
66 | examples: [
67 | 'presionar botón del dispositivo HOME',
68 | 'pulsar botón del dispositivo SIRI',
69 | 'presionar botón físico HOME',
70 | 'press device button HOME',
71 | 'push device button SIRI',
72 | 'tap device button LOCK'
73 | ],
74 | parameterExtractors: {
75 | button: (match) => match.groups?.button?.trim().toUpperCase() as any
76 | }
77 | },
78 | {
79 | command: 'input text',
80 | patterns: [
81 | /introducir\s+texto\s+(?<text>.+)/i,
82 | /escribir\s+texto\s+(?<text>.+)/i,
83 | /input\s+text\s+(?<text>.+)/i,
84 | /type\s+text\s+(?<text>.+)/i,
85 | /enter\s+text\s+(?<text>.+)/i
86 | ],
87 | description: 'Inputs text in the simulator',
88 | requiredParameters: ['text'],
89 | optionalParameters: ['sessionId'],
90 | examples: [
91 | 'introducir texto Hola mundo',
92 | 'escribir texto Prueba de texto',
93 | 'input text Hello world',
94 | 'type text Test message',
95 | 'enter text Hello'
96 | ],
97 | parameterExtractors: {
98 | text: (match) => match.groups?.text?.trim()
99 | }
100 | },
101 | {
102 | command: 'press key',
103 | patterns: [
104 | /presionar\s+(la\s+)?tecla\s+(?<keyCode>\d+)/i,
105 | /pulsar\s+(la\s+)?tecla\s+(?<keyCode>\d+)/i,
106 | /press\s+key\s+(?<keyCode>\d+)/i,
107 | /hit\s+key\s+(?<keyCode>\d+)/i,
108 | /type\s+key\s+(?<keyCode>\d+)/i
109 | ],
110 | description: 'Presses a specific key by its code',
111 | requiredParameters: ['keyCode'],
112 | optionalParameters: ['duration', 'sessionId'],
113 | examples: [
114 | 'presionar tecla 4',
115 | 'pulsar la tecla 65',
116 | 'press key 4',
117 | 'hit key 65',
118 | 'type key 13'
119 | ],
120 | parameterExtractors: {
121 | keyCode: (match) => parseInt(match.groups?.keyCode || '0', 10)
122 | }
123 | },
124 | {
125 | command: 'press key sequence',
126 | patterns: [
127 | /presionar\s+secuencia\s+de\s+teclas\s+(?<keyCodes>[\d\s]+)/i,
128 | /pulsar\s+secuencia\s+de\s+teclas\s+(?<keyCodes>[\d\s]+)/i,
129 | /press\s+key\s+sequence\s+(?<keyCodes>[\d\s]+)/i,
130 | /type\s+key\s+sequence\s+(?<keyCodes>[\d\s]+)/i,
131 | /enter\s+key\s+sequence\s+(?<keyCodes>[\d\s]+)/i
132 | ],
133 | description: 'Presses a sequence of keys',
134 | requiredParameters: ['keyCodes'],
135 | optionalParameters: ['sessionId'],
136 | examples: [
137 | 'presionar secuencia de teclas 4 5 6',
138 | 'pulsar secuencia de teclas 65 66 67',
139 | 'press key sequence 4 5 6',
140 | 'type key sequence 65 66 67',
141 | 'enter key sequence 13 14 15'
142 | ],
143 | parameterExtractors: {
144 | keyCodes: (match) => match.groups?.keyCodes?.trim().split(/\s+/).map(k => parseInt(k, 10))
145 | }
146 | }
147 | ];
148 | }
149 |
```
--------------------------------------------------------------------------------
/src/adapters/ParserToOrchestrator.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { ParseResult } from '../parser/interfaces/IParser.js';
5 | import {
6 | CommandType,
7 | IOrchestratorCommand,
8 | CommandFactory
9 | } from '../orchestrator/interfaces/IOrchestratorCommand.js';
10 |
11 | /**
12 | * Adapter that converts parser results into orchestrator commands
13 | */
14 | export class ParserToOrchestrator {
15 | private commandFactory: CommandFactory;
16 | private commandMappings: Record<string, CommandType>;
17 |
18 | /**
19 | * Constructor
20 | * @param commandFactory Command factory to create IOrchestratorCommand instances
21 | */
22 | constructor(commandFactory: CommandFactory) {
23 | this.commandFactory = commandFactory;
24 |
25 | // Mapping natural language commands to CommandType
26 | this.commandMappings = {
27 | // Simulator management commands
28 | 'crear sesión': CommandType.CREATE_SIMULATOR_SESSION,
29 | 'crear simulador': CommandType.CREATE_SIMULATOR_SESSION,
30 | 'iniciar simulador': CommandType.CREATE_SIMULATOR_SESSION,
31 | 'terminar sesión': CommandType.TERMINATE_SIMULATOR_SESSION,
32 | 'cerrar simulador': CommandType.TERMINATE_SIMULATOR_SESSION,
33 | 'listar simuladores': CommandType.LIST_AVAILABLE_SIMULATORS,
34 | 'mostrar simuladores': CommandType.LIST_AVAILABLE_SIMULATORS,
35 | 'listar simuladores arrancados': CommandType.LIST_BOOTED_SIMULATORS,
36 | 'arrancar simulador': CommandType.BOOT_SIMULATOR,
37 | 'apagar simulador': CommandType.SHUTDOWN_SIMULATOR,
38 |
39 | // Application management commands
40 | 'instalar app': CommandType.INSTALL_APP,
41 | 'instalar aplicación': CommandType.INSTALL_APP,
42 | 'lanzar app': CommandType.LAUNCH_APP,
43 | 'abrir app': CommandType.LAUNCH_APP,
44 | 'iniciar app': CommandType.LAUNCH_APP,
45 | 'cerrar app': CommandType.TERMINATE_APP,
46 | 'terminar app': CommandType.TERMINATE_APP,
47 |
48 | // UI interaction commands
49 | 'tap': CommandType.TAP,
50 | 'tocar': CommandType.TAP,
51 | 'pulsar': CommandType.TAP,
52 | 'swipe': CommandType.SWIPE,
53 | 'deslizar': CommandType.SWIPE,
54 |
55 | // Screenshot and logs commands
56 | 'capturar pantalla': CommandType.TAKE_SCREENSHOT,
57 | 'screenshot': CommandType.TAKE_SCREENSHOT,
58 | 'captura': CommandType.TAKE_SCREENSHOT,
59 | 'logs': CommandType.GET_SYSTEM_LOGS,
60 | 'logs del sistema': CommandType.GET_SYSTEM_LOGS,
61 | 'logs de app': CommandType.GET_APP_LOGS,
62 |
63 | // Verification commands
64 | 'verificar simulador': CommandType.IS_SIMULATOR_BOOTED,
65 | 'comprobar simulador': CommandType.IS_SIMULATOR_BOOTED,
66 | 'verificar app': CommandType.IS_APP_INSTALLED,
67 | 'comprobar app': CommandType.IS_APP_INSTALLED
68 | };
69 | }
70 |
71 | /**
72 | * Converts a parser result into an orchestrator command
73 | * @param parseResult Parser result
74 | * @returns Orchestrator command
75 | */
76 | public convertToCommand(parseResult: ParseResult): IOrchestratorCommand {
77 | // Determine command type
78 | const commandType = this.mapToCommandType(parseResult.command);
79 |
80 | // Convert parameters
81 | const parameters = this.convertParameters(commandType, parseResult.parameters);
82 |
83 | // Create and return command
84 | return this.commandFactory.createCommand(
85 | commandType,
86 | parameters,
87 | `Command generated from: "${parseResult.originalText}"`
88 | );
89 | }
90 |
91 | /**
92 | * Maps a natural language command to a CommandType
93 | * @param naturalCommand Natural language command
94 | * @returns Corresponding CommandType
95 | */
96 | private mapToCommandType(naturalCommand: string): CommandType {
97 | const lowerCommand = naturalCommand.toLowerCase();
98 |
99 | // Look for exact match
100 | if (this.commandMappings[lowerCommand]) {
101 | return this.commandMappings[lowerCommand];
102 | }
103 |
104 | // Look for partial match
105 | for (const [key, value] of Object.entries(this.commandMappings)) {
106 | if (lowerCommand.includes(key)) {
107 | return value;
108 | }
109 | }
110 |
111 | // If no match found, throw error
112 | throw new Error(`Could not map command "${naturalCommand}" to a CommandType`);
113 | }
114 |
115 | /**
116 | * Converts parser parameters to orchestrator parameters
117 | * @param commandType Command type
118 | * @param parserParameters Parser parameters
119 | * @returns Orchestrator parameters
120 | */
121 | private convertParameters(
122 | commandType: CommandType,
123 | parserParameters: Record<string, any>
124 | ): Record<string, any> {
125 | // Clone parameters to avoid modifying the original
126 | const parameters = { ...parserParameters };
127 |
128 | // Convert specific parameters based on command type
129 | switch (commandType) {
130 | case CommandType.TAP:
131 | // Ensure x and y are numbers
132 | if (parameters.x !== undefined) {
133 | parameters.x = Number(parameters.x);
134 | }
135 | if (parameters.y !== undefined) {
136 | parameters.y = Number(parameters.y);
137 | }
138 | break;
139 |
140 | case CommandType.SWIPE:
141 | // Ensure coordinates are numbers
142 | if (parameters.startX !== undefined) {
143 | parameters.startX = Number(parameters.startX);
144 | }
145 | if (parameters.startY !== undefined) {
146 | parameters.startY = Number(parameters.startY);
147 | }
148 | if (parameters.endX !== undefined) {
149 | parameters.endX = Number(parameters.endX);
150 | }
151 | if (parameters.endY !== undefined) {
152 | parameters.endY = Number(parameters.endY);
153 | }
154 | if (parameters.duration !== undefined) {
155 | parameters.duration = Number(parameters.duration);
156 | }
157 | break;
158 |
159 | case CommandType.CREATE_SIMULATOR_SESSION:
160 | // Convert autoboot to boolean if it's a string
161 | if (typeof parameters.autoboot === 'string') {
162 | parameters.autoboot = parameters.autoboot.toLowerCase() === 'true';
163 | }
164 | break;
165 | }
166 |
167 | return parameters;
168 | }
169 | }
170 |
```
--------------------------------------------------------------------------------
/src/parser/__tests__/NLParser.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NLParser } from '../NLParser.js';
2 | import { ParseResult, ValidationResult } from '../interfaces/IParser.js';
3 |
4 | // Add Jest types to global scope
5 | declare global {
6 | // eslint-disable-next-line @typescript-eslint/no-namespace
7 | namespace jest {
8 | interface Matchers<R> {
9 | [key: string]: any;
10 | }
11 | }
12 | }
13 |
14 | describe('NLParser', () => {
15 | let parser: NLParser;
16 |
17 | beforeEach(() => {
18 | parser = new NLParser();
19 | });
20 |
21 | describe('parseInstruction', () => {
22 | it('should parse a valid instruction', async () => {
23 | const result = await parser.parseInstruction('launch app com.example.app');
24 | expect(result).toBeDefined();
25 | expect(result.command).toBe('launch app');
26 | expect(result.parameters).toHaveProperty('bundleId');
27 | expect(result.parameters.bundleId).toBe('com.example.app');
28 | });
29 |
30 | it('should handle unknown instructions', async () => {
31 | try {
32 | await parser.parseInstruction('invalid command here');
33 | fail('Should have thrown an error');
34 | } catch (error: any) {
35 | expect(error).toBeDefined();
36 | expect(error.message).toContain('Could not understand the instruction');
37 | }
38 | });
39 | });
40 |
41 | describe('validateInstruction', () => {
42 | it('should validate a complete instruction', async () => {
43 | const parseResult: ParseResult = {
44 | command: 'launch app',
45 | parameters: {
46 | bundleId: 'com.example.app'
47 | },
48 | confidence: 1.0,
49 | originalText: 'launch app com.example.app'
50 | };
51 |
52 | const result = await parser.validateInstruction(parseResult);
53 | expect(result.isValid).toBe(true);
54 | });
55 |
56 | it('should detect missing required parameters', async () => {
57 | const parseResult: ParseResult = {
58 | command: 'launch app',
59 | parameters: {},
60 | confidence: 1.0,
61 | originalText: 'launch app'
62 | };
63 |
64 | const result = await parser.validateInstruction(parseResult);
65 | expect(result.isValid).toBe(false);
66 | expect(result.errorMessage).toContain('Missing required parameters');
67 | });
68 |
69 | it('should detect unrecognized commands', async () => {
70 | const parseResult: ParseResult = {
71 | command: 'invalid-command',
72 | parameters: {},
73 | confidence: 0.0,
74 | originalText: 'invalid-command'
75 | };
76 |
77 | const result = await parser.validateInstruction(parseResult);
78 | expect(result.isValid).toBe(false);
79 | expect(result.errorMessage).toContain('Unrecognized command');
80 | });
81 | });
82 |
83 | describe('normalizeParameters', () => {
84 | it('should normalize numeric string values to numbers', async () => {
85 | const parseResult: ParseResult = {
86 | command: 'set-location',
87 | parameters: {
88 | latitude: '37.7749',
89 | longitude: '-122.4194'
90 | },
91 | confidence: 1.0,
92 | originalText: 'set location 37.7749 -122.4194'
93 | };
94 |
95 | const result = await parser.normalizeParameters(parseResult);
96 | expect(typeof result.parameters.latitude).toBe('number');
97 | expect(typeof result.parameters.longitude).toBe('number');
98 | expect(result.parameters.latitude).toBe(37.7749);
99 | expect(result.parameters.longitude).toBe(-122.4194);
100 | });
101 |
102 | it('should normalize boolean string values to booleans', async () => {
103 | const parseResult: ParseResult = {
104 | command: 'set-preference',
105 | parameters: {
106 | enabled: 'true',
107 | disabled: 'false'
108 | },
109 | confidence: 1.0,
110 | originalText: 'set preference enabled true disabled false'
111 | };
112 |
113 | const result = await parser.normalizeParameters(parseResult);
114 | expect(typeof result.parameters.enabled).toBe('boolean');
115 | expect(typeof result.parameters.disabled).toBe('boolean');
116 | expect(result.parameters.enabled).toBe(true);
117 | expect(result.parameters.disabled).toBe(false);
118 | });
119 |
120 | it('should preserve non-numeric and non-boolean string values', async () => {
121 | const parseResult: ParseResult = {
122 | command: 'test-command',
123 | parameters: {
124 | text: 'hello world',
125 | mixedValue: '123abc'
126 | },
127 | confidence: 1.0,
128 | originalText: 'test command text "hello world" mixedValue 123abc'
129 | };
130 |
131 | const result = await parser.normalizeParameters(parseResult);
132 | expect(typeof result.parameters.text).toBe('string');
133 | expect(typeof result.parameters.mixedValue).toBe('string');
134 | expect(result.parameters.text).toBe('hello world');
135 | expect(result.parameters.mixedValue).toBe('123abc');
136 | });
137 | });
138 |
139 | describe('suggestCompletions', () => {
140 | it('should suggest completions for partial text', async () => {
141 | const suggestions = await parser.suggestCompletions('launch');
142 | expect(Array.isArray(suggestions)).toBe(true);
143 | expect(suggestions.length).toBeGreaterThan(0);
144 | expect(suggestions.some(s => s.startsWith('launch'))).toBe(true);
145 | });
146 |
147 | it('should return empty array for completely irrelevant text', async () => {
148 | const suggestions = await parser.suggestCompletions('xyzabc123');
149 | expect(Array.isArray(suggestions)).toBe(true);
150 | expect(suggestions.length).toBe(0);
151 | });
152 | });
153 |
154 | describe('getSupportedCommands', () => {
155 | it('should return a list of supported commands', async () => {
156 | const commands = await parser.getSupportedCommands();
157 | expect(Array.isArray(commands)).toBe(true);
158 | expect(commands.length).toBeGreaterThan(0);
159 |
160 | // Check command structure
161 | const command = commands[0];
162 | expect(command).toHaveProperty('command');
163 | expect(command).toHaveProperty('description');
164 | expect(command).toHaveProperty('requiredParameters');
165 | expect(command).toHaveProperty('optionalParameters');
166 | expect(Array.isArray(command.requiredParameters)).toBe(true);
167 | expect(Array.isArray(command.optionalParameters)).toBe(true);
168 | });
169 | });
170 | });
171 |
```
--------------------------------------------------------------------------------
/src/adapters/OrchestratorToIDB.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import {
5 | CommandType,
6 | IOrchestratorCommand,
7 | CommandResult
8 | } from '../orchestrator/interfaces/IOrchestratorCommand.js';
9 | import { IIDBManager } from '../idb/interfaces/IIDBManager.js';
10 |
11 | /**
12 | * Adapter that converts orchestrator commands into IDBManager calls
13 | */
14 | export class OrchestratorToIDB {
15 | private idbManager: IIDBManager;
16 |
17 | /**
18 | * Constructor
19 | * @param idbManager IDBManager instance to execute commands
20 | */
21 | constructor(idbManager: IIDBManager) {
22 | this.idbManager = idbManager;
23 | }
24 |
25 | /**
26 | * Executes an orchestrator command using IDBManager
27 | * @param command Command to execute
28 | * @param sessionId Simulator session ID (optional)
29 | * @returns Execution result
30 | */
31 | public async executeCommand(command: IOrchestratorCommand, sessionId?: string): Promise<CommandResult> {
32 | try {
33 | const startTime = Date.now();
34 | let result: any;
35 |
36 | // Execute command based on its type
37 | switch (command.type) {
38 | // Simulator management commands
39 | case CommandType.CREATE_SIMULATOR_SESSION:
40 | result = await this.idbManager.createSimulatorSession(command.parameters);
41 | break;
42 |
43 | case CommandType.TERMINATE_SIMULATOR_SESSION:
44 | await this.idbManager.terminateSimulatorSession(
45 | command.parameters.sessionId || sessionId || ''
46 | );
47 | result = { sessionId: command.parameters.sessionId || sessionId };
48 | break;
49 |
50 | case CommandType.LIST_AVAILABLE_SIMULATORS:
51 | result = await this.idbManager.listAvailableSimulators();
52 | break;
53 |
54 | case CommandType.LIST_BOOTED_SIMULATORS:
55 | result = await this.idbManager.listBootedSimulators();
56 | break;
57 |
58 | case CommandType.BOOT_SIMULATOR:
59 | await this.idbManager.bootSimulatorByUDID(command.parameters.udid);
60 | result = { udid: command.parameters.udid };
61 | break;
62 |
63 | case CommandType.SHUTDOWN_SIMULATOR:
64 | if (command.parameters.udid) {
65 | await this.idbManager.shutdownSimulatorByUDID(command.parameters.udid);
66 | result = { udid: command.parameters.udid };
67 | } else {
68 | await this.idbManager.shutdownSimulator(
69 | command.parameters.sessionId || sessionId || ''
70 | );
71 | result = { sessionId: command.parameters.sessionId || sessionId };
72 | }
73 | break;
74 |
75 | // Application management commands
76 | case CommandType.INSTALL_APP:
77 | result = await this.idbManager.installApp(
78 | command.parameters.sessionId || sessionId || '',
79 | command.parameters.appPath
80 | );
81 | break;
82 |
83 | case CommandType.LAUNCH_APP:
84 | await this.idbManager.launchApp(
85 | command.parameters.sessionId || sessionId || '',
86 | command.parameters.bundleId
87 | );
88 | result = { bundleId: command.parameters.bundleId };
89 | break;
90 |
91 | case CommandType.TERMINATE_APP:
92 | await this.idbManager.terminateApp(
93 | command.parameters.sessionId || sessionId || '',
94 | command.parameters.bundleId
95 | );
96 | result = { bundleId: command.parameters.bundleId };
97 | break;
98 |
99 | // UI interaction commands
100 | case CommandType.TAP:
101 | await this.idbManager.tap(
102 | command.parameters.sessionId || sessionId || '',
103 | command.parameters.x,
104 | command.parameters.y
105 | );
106 | result = { x: command.parameters.x, y: command.parameters.y };
107 | break;
108 |
109 | case CommandType.SWIPE:
110 | await this.idbManager.swipe(
111 | command.parameters.sessionId || sessionId || '',
112 | command.parameters.startX,
113 | command.parameters.startY,
114 | command.parameters.endX,
115 | command.parameters.endY,
116 | command.parameters.duration
117 | );
118 | result = {
119 | startX: command.parameters.startX,
120 | startY: command.parameters.startY,
121 | endX: command.parameters.endX,
122 | endY: command.parameters.endY
123 | };
124 | break;
125 |
126 | // Screenshot and logging commands
127 | case CommandType.TAKE_SCREENSHOT:
128 | result = await this.idbManager.takeScreenshot(
129 | command.parameters.sessionId || sessionId || '',
130 | command.parameters.outputPath
131 | );
132 | break;
133 |
134 | case CommandType.GET_SYSTEM_LOGS:
135 | result = await this.idbManager.getSystemLogs(
136 | command.parameters.sessionId || sessionId || '',
137 | command.parameters.options
138 | );
139 | break;
140 |
141 | case CommandType.GET_APP_LOGS:
142 | result = await this.idbManager.getAppLogs(
143 | command.parameters.sessionId || sessionId || '',
144 | command.parameters.bundleId
145 | );
146 | break;
147 |
148 | // Verification commands
149 | case CommandType.IS_SIMULATOR_BOOTED:
150 | result = await this.idbManager.isSimulatorBooted(
151 | command.parameters.sessionId || sessionId || ''
152 | );
153 | break;
154 |
155 | case CommandType.IS_APP_INSTALLED:
156 | result = await this.idbManager.isAppInstalled(
157 | command.parameters.sessionId || sessionId || '',
158 | command.parameters.bundleId
159 | );
160 | break;
161 |
162 | default:
163 | throw new Error(`Unsupported command type: ${command.type}`);
164 | }
165 |
166 | // Create and return the result
167 | return {
168 | success: true,
169 | data: result,
170 | timestamp: Date.now()
171 | };
172 | } catch (error: any) {
173 | // Handle errors
174 | console.error(`Error executing command ${command.type}:`, error);
175 |
176 | // If there's a custom error handler, use it
177 | if (command.onError) {
178 | return command.onError(error, { sessionId });
179 | }
180 |
181 | // Return error result
182 | return {
183 | success: false,
184 | error: error.message || 'Unknown error',
185 | timestamp: Date.now()
186 | };
187 | }
188 | }
189 | }
190 |
```
--------------------------------------------------------------------------------
/src/parser/commands/MiscCommands.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { BaseCommandDefinition, CommandDefinition } from './BaseCommandDefinition.js';
5 |
6 | export class MiscCommands extends BaseCommandDefinition {
7 | protected definitions: CommandDefinition[] = [
8 | {
9 | command: 'install dylib',
10 | patterns: [
11 | /instalar\s+dylib\s+(?<dylibPath>[^\s,]+)/i,
12 | /install\s+dylib\s+(?<dylibPath>[^\s,]+)/i,
13 | /add\s+dylib\s+(?<dylibPath>[^\s,]+)/i,
14 | /load\s+dylib\s+(?<dylibPath>[^\s,]+)/i
15 | ],
16 | description: 'Installs a dynamic library (.dylib)',
17 | requiredParameters: ['dylibPath'],
18 | optionalParameters: ['sessionId'],
19 | examples: [
20 | 'instalar dylib /ruta/a/lib.dylib',
21 | 'install dylib /tmp/library.dylib',
22 | 'add dylib /path/to/lib.dylib',
23 | 'load dylib /path/to/library.dylib'
24 | ],
25 | parameterExtractors: {
26 | dylibPath: (match) => match.groups?.dylibPath?.trim()
27 | }
28 | },
29 | {
30 | command: 'open url',
31 | patterns: [
32 | /abrir\s+url\s+(?<url>[^\s]+)/i,
33 | /open\s+url\s+(?<url>[^\s]+)/i,
34 | /navegar\s+a\s+(?<url>[^\s]+)/i,
35 | /navigate\s+to\s+(?<url>[^\s]+)/i,
36 | /browse\s+to\s+(?<url>[^\s]+)/i
37 | ],
38 | description: 'Opens a URL in the simulator',
39 | requiredParameters: ['url'],
40 | optionalParameters: ['sessionId'],
41 | examples: [
42 | 'abrir url https://example.com',
43 | 'open url https://google.com',
44 | 'navegar a https://apple.com',
45 | 'navigate to https://example.com',
46 | 'browse to https://apple.com'
47 | ],
48 | parameterExtractors: {
49 | url: (match) => match.groups?.url?.trim()
50 | }
51 | },
52 | {
53 | command: 'clear keychain',
54 | patterns: [
55 | /limpiar\s+keychain/i,
56 | /clear\s+keychain/i,
57 | /borrar\s+keychain/i,
58 | /reset\s+keychain/i,
59 | /empty\s+keychain/i
60 | ],
61 | description: 'Clears the simulator keychain',
62 | requiredParameters: [],
63 | optionalParameters: ['sessionId'],
64 | examples: [
65 | 'limpiar keychain',
66 | 'clear keychain',
67 | 'borrar keychain',
68 | 'reset keychain',
69 | 'empty keychain'
70 | ],
71 | parameterExtractors: {}
72 | },
73 | {
74 | command: 'set location',
75 | patterns: [
76 | /establecer\s+ubicación\s+(?<latitude>-?\d+(\.\d+)?)\s*,\s*(?<longitude>-?\d+(\.\d+)?)/i,
77 | /set\s+location\s+(?<latitude>-?\d+(\.\d+)?)\s*,\s*(?<longitude>-?\d+(\.\d+)?)/i,
78 | /cambiar\s+ubicación\s+a\s+(?<latitude>-?\d+(\.\d+)?)\s*,\s*(?<longitude>-?\d+(\.\d+)?)/i,
79 | /update\s+location\s+(?<latitude>-?\d+(\.\d+)?)\s*,\s*(?<longitude>-?\d+(\.\d+)?)/i,
80 | /change\s+location\s+to\s+(?<latitude>-?\d+(\.\d+)?)\s*,\s*(?<longitude>-?\d+(\.\d+)?)/i
81 | ],
82 | description: 'Sets the simulator location',
83 | requiredParameters: ['latitude', 'longitude'],
84 | optionalParameters: ['sessionId'],
85 | examples: [
86 | 'establecer ubicación 37.7749, -122.4194',
87 | 'set location 40.7128, -74.0060',
88 | 'cambiar ubicación a 51.5074, -0.1278',
89 | 'update location 48.8566, 2.3522',
90 | 'change location to 35.6762, 139.6503'
91 | ],
92 | parameterExtractors: {
93 | latitude: (match) => parseFloat(match.groups?.latitude || '0'),
94 | longitude: (match) => parseFloat(match.groups?.longitude || '0')
95 | }
96 | },
97 | {
98 | command: 'add media',
99 | patterns: [
100 | /añadir\s+media\s+(?<mediaPaths>.+)/i,
101 | /add\s+media\s+(?<mediaPaths>.+)/i,
102 | /importar\s+multimedia\s+(?<mediaPaths>.+)/i,
103 | /import\s+media\s+(?<mediaPaths>.+)/i,
104 | /upload\s+media\s+(?<mediaPaths>.+)/i
105 | ],
106 | description: 'Adds media files to the simulator camera roll',
107 | requiredParameters: ['mediaPaths'],
108 | optionalParameters: ['sessionId'],
109 | examples: [
110 | 'añadir media /ruta/imagen.jpg /ruta/video.mp4',
111 | 'add media /tmp/photo.png',
112 | 'importar multimedia /ruta/a/fotos/*.jpg',
113 | 'import media /path/to/photos/*.jpg',
114 | 'upload media /path/video.mp4'
115 | ],
116 | parameterExtractors: {
117 | mediaPaths: (match) => match.groups?.mediaPaths?.trim().split(/\s+/)
118 | }
119 | },
120 | {
121 | command: 'approve permissions',
122 | patterns: [
123 | /aprobar\s+permisos\s+(?<bundleId>[^\s,]+)\s+(?<permissions>.+)/i,
124 | /approve\s+permissions\s+(?<bundleId>[^\s,]+)\s+(?<permissions>.+)/i,
125 | /dar\s+permisos\s+a\s+(?<bundleId>[^\s,]+)\s+(?<permissions>.+)/i,
126 | /grant\s+permissions\s+(?<bundleId>[^\s,]+)\s+(?<permissions>.+)/i,
127 | /allow\s+permissions\s+(?<bundleId>[^\s,]+)\s+(?<permissions>.+)/i
128 | ],
129 | description: 'Approves permissions for an application',
130 | requiredParameters: ['bundleId', 'permissions'],
131 | optionalParameters: ['sessionId'],
132 | examples: [
133 | 'aprobar permisos com.example.app photos camera',
134 | 'approve permissions com.apple.mobilesafari contacts',
135 | 'dar permisos a com.example.app photos',
136 | 'grant permissions com.example.app camera',
137 | 'allow permissions com.example.app location'
138 | ],
139 | parameterExtractors: {
140 | bundleId: (match) => match.groups?.bundleId?.trim(),
141 | permissions: (match) => match.groups?.permissions?.trim().split(/\s+/)
142 | }
143 | },
144 | {
145 | command: 'update contacts',
146 | patterns: [
147 | /actualizar\s+contactos\s+(?<dbPath>[^\s,]+)/i,
148 | /update\s+contacts\s+(?<dbPath>[^\s,]+)/i,
149 | /importar\s+contactos\s+(?<dbPath>[^\s,]+)/i,
150 | /import\s+contacts\s+(?<dbPath>[^\s,]+)/i,
151 | /load\s+contacts\s+(?<dbPath>[^\s,]+)/i
152 | ],
153 | description: 'Updates the simulator contacts database',
154 | requiredParameters: ['dbPath'],
155 | optionalParameters: ['sessionId'],
156 | examples: [
157 | 'actualizar contactos /ruta/a/contactos.sqlite',
158 | 'update contacts /tmp/contacts.db',
159 | 'importar contactos /ruta/contacts.sqlite',
160 | 'import contacts /path/to/contacts.db',
161 | 'load contacts /path/contacts.sqlite'
162 | ],
163 | parameterExtractors: {
164 | dbPath: (match) => match.groups?.dbPath?.trim()
165 | }
166 | }
167 | ];
168 | }
169 |
```
--------------------------------------------------------------------------------
/repolinter.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://raw.githubusercontent.com/todogroup/repolinter/master/rulesets/schema.json",
3 | "version": 2,
4 | "axioms": {
5 | "linguist": "language",
6 | "licensee": "license",
7 | "packagers": "packager"
8 | },
9 | "rules": {
10 | "license-file-exists": {
11 | "level": "error",
12 | "rule": {
13 | "type": "file-existence",
14 | "options": {
15 | "globsAny": ["LICENSE*", "COPYING*"],
16 | "nocase": true
17 | }
18 | }
19 | },
20 | "readme-file-exists": {
21 | "level": "error",
22 | "rule": {
23 | "type": "file-existence",
24 | "options": {
25 | "globsAny": ["README*"],
26 | "nocase": true
27 | }
28 | }
29 | },
30 | "contributing-file-exists": {
31 | "level": "error",
32 | "rule": {
33 | "type": "file-existence",
34 | "options": {
35 | "globsAny": ["{docs/,.github/,}CONTRIB*"],
36 | "nocase": true
37 | }
38 | }
39 | },
40 | "code-of-conduct-file-exists": {
41 | "level": "error",
42 | "rule": {
43 | "type": "file-existence",
44 | "options": {
45 | "globsAny": [
46 | "{docs/,.github/,}CODEOFCONDUCT*",
47 | "{docs/,.github/,}CODE-OF-CONDUCT*",
48 | "{docs/,.github/,}CODE_OF_CONDUCT*"
49 | ],
50 | "nocase": true
51 | }
52 | }
53 | },
54 | "changelog-file-exists": {
55 | "level": "error",
56 | "rule": {
57 | "type": "file-existence",
58 | "options": {
59 | "globsAny": ["CHANGELOG*"],
60 | "nocase": true
61 | }
62 | }
63 | },
64 | "security-file-exists": {
65 | "level": "error",
66 | "rule": {
67 | "type": "file-existence",
68 | "options": {
69 | "globsAny": ["{docs/,.github/,}SECURITY.md"]
70 | }
71 | }
72 | },
73 | "support-file-exists": {
74 | "level": "off",
75 | "rule": {
76 | "type": "file-existence",
77 | "options": {
78 | "globsAny": ["{docs/,.github/,}SUPPORT*"],
79 | "nocase": true
80 | }
81 | }
82 | },
83 | "readme-references-license": {
84 | "level": "off",
85 | "rule": {
86 | "type": "file-contents",
87 | "options": {
88 | "globsAll": ["README*"],
89 | "content": "license",
90 | "flags": "i"
91 | }
92 | }
93 | },
94 | "binaries-not-present": {
95 | "level": "error",
96 | "rule": {
97 | "type": "file-type-exclusion",
98 | "options": {
99 | "type": ["/*.exe", "/.dll", "!node_modules/"]
100 | }
101 | }
102 | },
103 | "test-directory-exists": {
104 | "level": "off",
105 | "rule": {
106 | "type": "directory-existence",
107 | "options": {
108 | "globsAny": ["/test", "/specs"],
109 | "nocase": true
110 | }
111 | }
112 | },
113 | "integrates-with-ci": {
114 | "level": "error",
115 | "rule": {
116 | "type": "file-existence",
117 | "options": {
118 | "globsAny": [
119 | ".gitlab-ci.yml",
120 | ".travis.yml",
121 | "appveyor.yml",
122 | ".appveyor.yml",
123 | "circle.yml",
124 | ".circleci/config.yml",
125 | "Jenkinsfile",
126 | ".drone.yml",
127 | ".github/workflows/",
128 | "azure-pipelines.yml"
129 | ]
130 | }
131 | }
132 | },
133 | "code-of-conduct-file-contains-email": {
134 | "level": "off",
135 | "rule": {
136 | "type": "file-contents",
137 | "options": {
138 | "globsAll": [
139 | "CODEOFCONDUCT",
140 | "CODE-OF-CONDUCT*",
141 | "CODE_OF_CONDUCT*",
142 | ".github/CODEOFCONDUCT*",
143 | ".github/CODE-OF-CONDUCT*",
144 | ".github/CODE_OF_CONDUCT*"
145 | ],
146 | "content": ".+@.+..+",
147 | "flags": "i",
148 | "human-readable-content": "email address"
149 | }
150 | }
151 | },
152 | "source-license-headers-exist": {
153 | "level": "warning",
154 | "rule": {
155 | "type": "file-starts-with",
156 | "options": {
157 | "globsAll": ["./**/*.go"],
158 | "lineCount": 5,
159 | "patterns": ["Copyright", "License"],
160 | "flags": "i"
161 | }
162 | }
163 | },
164 | "github-issue-template-exists": {
165 | "level": "error",
166 | "rule": {
167 | "type": "file-existence",
168 | "options": {
169 | "dirs": true,
170 | "globsAny": ["ISSUE_TEMPLATE", ".github/ISSUE_TEMPLATE*"]
171 | }
172 | }
173 | },
174 | "github-pull-request-template-exists": {
175 | "level": "off",
176 | "rule": {
177 | "type": "file-existence",
178 | "options": {
179 | "dirs": true,
180 | "globsAny": [
181 | "PULL_REQUEST_TEMPLATE*",
182 | ".github/PULL_REQUEST_TEMPLATE*"
183 | ]
184 | }
185 | }
186 | },
187 | "javascript-package-metadata-exists": {
188 | "level": "error",
189 | "where": ["language=javascript"],
190 | "rule": {
191 | "type": "file-existence",
192 | "options": {
193 | "globsAny": ["package.json"]
194 | }
195 | }
196 | },
197 | "ruby-package-metadata-exists": {
198 | "level": "error",
199 | "where": ["language=ruby"],
200 | "rule": {
201 | "type": "file-existence",
202 | "options": {
203 | "globsAny": ["Gemfile"]
204 | }
205 | }
206 | },
207 | "java-package-metadata-exists": {
208 | "level": "error",
209 | "where": ["language=java"],
210 | "rule": {
211 | "type": "file-existence",
212 | "options": {
213 | "globsAny": ["pom.xml", "build.xml", "build.gradle"]
214 | }
215 | }
216 | },
217 | "python-package-metadata-exists": {
218 | "level": "error",
219 | "where": ["language=python"],
220 | "rule": {
221 | "type": "file-existence",
222 | "options": {
223 | "globsAny": ["setup.py", "requirements.txt"]
224 | }
225 | }
226 | },
227 | "objective-c-package-metadata-exists": {
228 | "level": "error",
229 | "where": ["language=objective-c"],
230 | "rule": {
231 | "type": "file-existence",
232 | "options": {
233 | "globsAny": ["Cartfile", "Podfile", ".podspec"]
234 | }
235 | }
236 | },
237 | "swift-package-metadata-exists": {
238 | "level": "error",
239 | "where": ["language=swift"],
240 | "rule": {
241 | "type": "file-existence",
242 | "options": {
243 | "globsAny": ["Package.swift"]
244 | }
245 | }
246 | },
247 | "erlang-package-metadata-exists": {
248 | "level": "error",
249 | "where": ["language=erlang"],
250 | "rule": {
251 | "type": "file-existence",
252 | "options": {
253 | "globsAny": ["rebar.config"]
254 | }
255 | }
256 | },
257 | "elixir-package-metadata-exists": {
258 | "level": "error",
259 | "where": ["language=elixir"],
260 | "rule": {
261 | "type": "file-existence",
262 | "options": {
263 | "globsAny": ["mix.exs"]
264 | }
265 | }
266 | },
267 | "license-detectable-by-licensee": {
268 | "level": "off",
269 | "where": ["license="],
270 | "rule": {
271 | "type": "license-detectable-by-licensee",
272 | "options": {}
273 | }
274 | },
275 | "notice-file-exists": {
276 | "level": "error",
277 | "where": ["license=Apache-2.0"],
278 | "rule": {
279 | "type": "file-existence",
280 | "options": {
281 | "globsAny": ["NOTICE*"],
282 | "fail-message": "The NOTICE file is described in section 4.4 of the Apache License version 2.0. Its presence is not mandated by the license itself, but by ASF policy."
283 | }
284 | }
285 | },
286 | "best-practices-badge-present": {
287 | "level": "off",
288 | "rule": {
289 | "type": "best-practices-badge-present"
290 | }
291 | },
292 | "internal-file-not-exists": {
293 | "level": "off",
294 | "rule": {
295 | "type": "file-not-exists",
296 | "options": {
297 | "globsAll": [
298 | ".secrets.baseline",
299 | "sherpa-config.yml",
300 | ".snyk",
301 | "sonar-project.properties",
302 | ".drafterconfig.yml",
303 | "application-configmap.yml",
304 | "application-secret.yml"
305 | ],
306 | "nocase": true
307 | }
308 | }
309 | }
310 | }
311 | }
```
--------------------------------------------------------------------------------
/src/orchestrator/__tests__/MCPOrchestrator.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest } from '@jest/globals';
2 | import { MCPOrchestrator } from '../MCPOrchestrator.js';
3 | import { IParser, ParseResult, ValidationResult } from '../../parser/interfaces/IParser.js';
4 | import { IIDBManager } from '../../idb/interfaces/IIDBManager.js';
5 | import { CommandType, CommandResult, IOrchestratorCommand } from '../interfaces/IOrchestratorCommand.js';
6 |
7 | // Mock implementations
8 | const mockParser: jest.Mocked<IParser> = {
9 | parseInstruction: jest.fn(),
10 | validateInstruction: jest.fn(),
11 | normalizeParameters: jest.fn(),
12 | getSupportedCommands: jest.fn(),
13 | suggestCompletions: jest.fn()
14 | };
15 |
16 | const mockIDBManager: jest.Mocked<IIDBManager> = {
17 | // Required simulator management methods
18 | createSimulatorSession: jest.fn(),
19 | terminateSimulatorSession: jest.fn(),
20 | listAvailableSimulators: jest.fn(),
21 | listBootedSimulators: jest.fn(),
22 | listSimulatorSessions: jest.fn(),
23 | bootSimulatorByUDID: jest.fn(),
24 | shutdownSimulatorByUDID: jest.fn(),
25 | shutdownSimulator: jest.fn(),
26 | isSimulatorBooted: jest.fn(),
27 |
28 | // Required app management methods
29 | installApp: jest.fn(),
30 | launchApp: jest.fn(),
31 | terminateApp: jest.fn(),
32 | isAppInstalled: jest.fn(),
33 |
34 | // Required UI interaction methods
35 | tap: jest.fn(),
36 | swipe: jest.fn(),
37 |
38 | // Required screenshots and logs methods
39 | takeScreenshot: jest.fn(),
40 | getSystemLogs: jest.fn(),
41 | getAppLogs: jest.fn()
42 | };
43 |
44 | describe('MCPOrchestrator', () => {
45 | let orchestrator: MCPOrchestrator;
46 |
47 | beforeEach(() => {
48 | // Clear all mocks
49 | jest.clearAllMocks();
50 | orchestrator = new MCPOrchestrator(mockParser, mockIDBManager);
51 | });
52 |
53 | describe('processInstruction', () => {
54 | it('should process a valid instruction successfully', async () => {
55 | const instruction = 'lanzar app com.example.app';
56 | const parseResult: ParseResult = {
57 | command: 'lanzar app',
58 | parameters: { bundleId: 'com.example.app' },
59 | confidence: 1.0,
60 | originalText: instruction
61 | };
62 | const validationResult: ValidationResult = {
63 | isValid: true
64 | };
65 |
66 | mockParser.parseInstruction.mockResolvedValue(parseResult);
67 | mockParser.validateInstruction.mockResolvedValue(validationResult);
68 | mockParser.normalizeParameters.mockResolvedValue(parseResult);
69 | mockIDBManager.launchApp.mockResolvedValue();
70 |
71 | const result = await orchestrator.processInstruction(instruction);
72 | expect(result.success).toBe(true);
73 | expect(mockParser.parseInstruction).toHaveBeenCalledWith(instruction);
74 | expect(mockParser.validateInstruction).toHaveBeenCalledWith(parseResult);
75 | expect(mockParser.normalizeParameters).toHaveBeenCalledWith(parseResult);
76 | });
77 |
78 | it('should handle invalid instructions', async () => {
79 | const instruction = 'invalid command';
80 | const parseResult: ParseResult = {
81 | command: 'unknown',
82 | parameters: {},
83 | confidence: 0.0,
84 | originalText: instruction
85 | };
86 | const validationResult: ValidationResult = {
87 | isValid: false,
88 | errorMessage: 'Invalid command'
89 | };
90 |
91 | mockParser.parseInstruction.mockResolvedValue(parseResult);
92 | mockParser.validateInstruction.mockResolvedValue(validationResult);
93 |
94 | const result = await orchestrator.processInstruction(instruction);
95 | expect(result.success).toBe(false);
96 | expect(result.error).toBe('Invalid command');
97 | });
98 | });
99 |
100 | describe('executeCommand', () => {
101 | it('should execute a simple command successfully', async () => {
102 | const command: IOrchestratorCommand = {
103 | type: CommandType.LAUNCH_APP,
104 | parameters: { bundleId: 'com.example.app' },
105 | id: '123',
106 | description: 'Launch app'
107 | };
108 |
109 | mockIDBManager.launchApp.mockResolvedValue();
110 |
111 | const result = await orchestrator.executeCommand(command);
112 | expect(result.success).toBe(true);
113 | });
114 |
115 | it('should execute a sequence command', async () => {
116 | const command: IOrchestratorCommand = {
117 | type: CommandType.SEQUENCE,
118 | parameters: {
119 | commands: [
120 | {
121 | type: CommandType.LAUNCH_APP,
122 | parameters: { bundleId: 'com.example.app' },
123 | id: '123',
124 | description: 'Launch app'
125 | },
126 | {
127 | type: CommandType.TAP,
128 | parameters: { x: 100, y: 200 },
129 | id: '456',
130 | description: 'Tap screen'
131 | }
132 | ],
133 | stopOnError: true
134 | },
135 | id: '789',
136 | description: 'Sequence command'
137 | };
138 |
139 | mockIDBManager.launchApp.mockResolvedValue();
140 | mockIDBManager.tap.mockResolvedValue();
141 |
142 | const result = await orchestrator.executeCommand(command);
143 | expect(result.success).toBe(true);
144 | expect(mockIDBManager.launchApp).toHaveBeenCalled();
145 | expect(mockIDBManager.tap).toHaveBeenCalled();
146 | });
147 | });
148 |
149 | describe('session management', () => {
150 | it('should manage session ID correctly', () => {
151 | const sessionId = 'test-session-123';
152 | orchestrator.setActiveSessionId(sessionId);
153 | expect(orchestrator.getActiveSessionId()).toBe(sessionId);
154 |
155 | orchestrator.setActiveSessionId(null);
156 | expect(orchestrator.getActiveSessionId()).toBeNull();
157 | });
158 |
159 | it('should update session ID after creating a simulator session', async () => {
160 | const command: IOrchestratorCommand = {
161 | type: CommandType.CREATE_SIMULATOR_SESSION,
162 | parameters: { udid: 'test-simulator' },
163 | id: '123',
164 | description: 'Create simulator session'
165 | };
166 |
167 | mockIDBManager.createSimulatorSession.mockResolvedValue('new-session-123');
168 |
169 | const result = await orchestrator.executeCommand(command);
170 | expect(result.success).toBe(true);
171 | expect(orchestrator.getActiveSessionId()).toBe('new-session-123');
172 | });
173 | });
174 |
175 | describe('command history', () => {
176 | it('should maintain command history', async () => {
177 | const command: IOrchestratorCommand = {
178 | type: CommandType.LAUNCH_APP,
179 | parameters: { bundleId: 'com.example.app' },
180 | id: '123',
181 | description: 'Launch app'
182 | };
183 |
184 | mockIDBManager.launchApp.mockResolvedValue();
185 |
186 | await orchestrator.executeCommand(command);
187 | const history = orchestrator.getCommandHistory();
188 |
189 | expect(history.length).toBe(1);
190 | expect(history[0].command).toEqual(command);
191 | expect(history[0].result.success).toBe(true);
192 | });
193 |
194 | it('should respect history limit', async () => {
195 | const command1: IOrchestratorCommand = {
196 | type: CommandType.LAUNCH_APP,
197 | parameters: { bundleId: 'com.example.app1' },
198 | id: '123',
199 | description: 'Launch app 1'
200 | };
201 |
202 | const command2: IOrchestratorCommand = {
203 | type: CommandType.LAUNCH_APP,
204 | parameters: { bundleId: 'com.example.app2' },
205 | id: '456',
206 | description: 'Launch app 2'
207 | };
208 |
209 | mockIDBManager.launchApp.mockResolvedValue();
210 |
211 | await orchestrator.executeCommand(command1);
212 | await orchestrator.executeCommand(command2);
213 |
214 | const limitedHistory = orchestrator.getCommandHistory(1);
215 | expect(limitedHistory.length).toBe(1);
216 | expect(limitedHistory[0].command).toEqual(command2);
217 | });
218 | });
219 |
220 | describe('event handling', () => {
221 | it('should handle event listeners correctly', () => {
222 | const mockListener = jest.fn();
223 | const event = 'testEvent';
224 | const data = { test: true };
225 |
226 | // Add listener
227 | orchestrator.on(event, mockListener);
228 | orchestrator['emit'](event, data);
229 | expect(mockListener).toHaveBeenCalledWith(data);
230 |
231 | // Remove listener
232 | orchestrator.off(event, mockListener);
233 | orchestrator['emit'](event, data);
234 | expect(mockListener).toHaveBeenCalledTimes(1);
235 | });
236 |
237 | it('should emit session events', async () => {
238 | const sessionCreatedListener = jest.fn();
239 | orchestrator.on('sessionCreated', sessionCreatedListener);
240 |
241 | const command: IOrchestratorCommand = {
242 | type: CommandType.CREATE_SIMULATOR_SESSION,
243 | parameters: { udid: 'test-simulator' },
244 | id: '123',
245 | description: 'Create simulator session'
246 | };
247 |
248 | mockIDBManager.createSimulatorSession.mockResolvedValue('new-session-123');
249 |
250 | await orchestrator.executeCommand(command);
251 | expect(sessionCreatedListener).toHaveBeenCalledWith({ sessionId: 'new-session-123' });
252 | });
253 | });
254 | });
255 |
```
--------------------------------------------------------------------------------
/LICENSES/Apache-2.0.txt:
--------------------------------------------------------------------------------
```
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
```
--------------------------------------------------------------------------------
/src/idb/interfaces/IIDBManager.ts:
--------------------------------------------------------------------------------
```typescript
1 | // SPDX-FileCopyrightText: © 2025 Industria de Diseño Textil S.A. INDITEX
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /**
5 | * IIDBManager - Interface for managing iOS simulators through idb
6 | *
7 | * This interface defines the necessary methods to interact with iOS simulators
8 | * using the idb tool (https://fbidb.io/docs/commands).
9 | */
10 |
11 | export interface SimulatorInfo {
12 | udid: string;
13 | name: string;
14 | state: 'Booted' | 'Shutdown' | 'Unknown';
15 | os: string;
16 | deviceType: string;
17 | }
18 |
19 | export interface AppInfo {
20 | bundleId: string;
21 | name: string;
22 | installedPath?: string;
23 | }
24 |
25 | export interface SessionConfig {
26 | deviceName?: string;
27 | platformVersion?: string;
28 | timeout?: number;
29 | autoboot?: boolean;
30 | }
31 |
32 | export type ButtonType = 'APPLE_PAY' | 'HOME' | 'LOCK' | 'SIDE_BUTTON' | 'SIRI';
33 |
34 | export interface CrashLogInfo {
35 | name: string;
36 | bundleId?: string;
37 | date: Date;
38 | path: string;
39 | }
40 |
41 | export interface AccessibilityInfo {
42 | identifier?: string;
43 | label?: string;
44 | frame: {
45 | x: number;
46 | y: number;
47 | width: number;
48 | height: number;
49 | };
50 | type?: string;
51 | value?: string;
52 | children?: AccessibilityInfo[];
53 | }
54 |
55 | export interface IIDBManager {
56 | // === Simulator Management ===
57 |
58 | /**
59 | * Initializes a new simulator session
60 | * @param config Optional configuration for the session
61 | * @returns Created session ID
62 | */
63 | createSimulatorSession(config?: SessionConfig): Promise<string>;
64 |
65 | /**
66 | * Terminates an existing simulator session
67 | * @param sessionId Session ID to terminate
68 | */
69 | terminateSimulatorSession(sessionId: string): Promise<void>;
70 |
71 | /**
72 | * Lists all available simulators
73 | * @returns List of simulator information
74 | */
75 | listAvailableSimulators(): Promise<SimulatorInfo[]>;
76 |
77 | /**
78 | * Lists currently booted simulators
79 | * @returns List of booted simulator information
80 | */
81 | listBootedSimulators(): Promise<SimulatorInfo[]>;
82 |
83 | /**
84 | * Boots a simulator by its UDID
85 | * @param udid UDID of the simulator to boot
86 | */
87 | bootSimulatorByUDID(udid: string): Promise<void>;
88 |
89 | /**
90 | * Shuts down a simulator by its UDID
91 | * @param udid UDID of the simulator to shut down
92 | */
93 | shutdownSimulatorByUDID(udid: string): Promise<void>;
94 |
95 | /**
96 | * Shuts down a simulator by its session ID
97 | * @param sessionId Session ID of the simulator to shut down
98 | */
99 | shutdownSimulator(sessionId: string): Promise<void>;
100 |
101 | /**
102 | * Lists active simulator sessions
103 | * @returns List of active session IDs
104 | */
105 | listSimulatorSessions(): Promise<string[]>;
106 |
107 | /**
108 | * Verifies if a simulator is booted
109 | * @param sessionId Session ID of the simulator
110 | * @returns true if booted, false otherwise
111 | */
112 | isSimulatorBooted(sessionId: string): Promise<boolean>;
113 |
114 | /**
115 | * Focuses the simulator window
116 | * @param sessionId Session ID of the simulator
117 | */
118 | focusSimulator?(sessionId: string): Promise<void>;
119 |
120 | // === Application Management ===
121 |
122 | /**
123 | * Installs an application on the simulator
124 | * @param sessionId Session ID of the simulator
125 | * @param appPath Path to the .app or .ipa file to install
126 | * @returns Information about the installed application
127 | */
128 | installApp(sessionId: string, appPath: string): Promise<AppInfo>;
129 |
130 | /**
131 | * Launches an application on the simulator
132 | * @param sessionId Session ID of the simulator
133 | * @param bundleId Bundle ID of the application to launch
134 | * @param env Environment variables (optional)
135 | * @param args Launch arguments (optional)
136 | */
137 | launchApp(
138 | sessionId: string,
139 | bundleId: string,
140 | env?: Record<string, string>,
141 | args?: string[]
142 | ): Promise<void>;
143 |
144 | /**
145 | * Terminates a running application
146 | * @param sessionId Session ID of the simulator
147 | * @param bundleId Bundle ID of the application to terminate
148 | */
149 | terminateApp(sessionId: string, bundleId: string): Promise<void>;
150 |
151 | /**
152 | * Uninstalls an application
153 | * @param sessionId Session ID of the simulator
154 | * @param bundleId Bundle ID of the application to uninstall
155 | */
156 | uninstallApp?(sessionId: string, bundleId: string): Promise<void>;
157 |
158 | /**
159 | * Lists installed applications
160 | * @param sessionId Session ID of the simulator
161 | * @returns List of application information
162 | */
163 | listApps?(sessionId: string): Promise<AppInfo[]>;
164 |
165 | /**
166 | * Verifies if an application is installed
167 | * @param sessionId Session ID of the simulator
168 | * @param bundleId Bundle ID of the application
169 | * @returns true if installed, false otherwise
170 | */
171 | isAppInstalled(sessionId: string, bundleId: string): Promise<boolean>;
172 |
173 | // === UI Interaction ===
174 |
175 | /**
176 | * Performs a tap at the specified coordinates
177 | * @param sessionId Session ID of the simulator
178 | * @param x X coordinate
179 | * @param y Y coordinate
180 | * @param duration Tap duration in milliseconds (optional)
181 | */
182 | tap(sessionId: string, x: number, y: number, duration?: number): Promise<void>;
183 |
184 | /**
185 | * Performs a swipe on the screen
186 | * @param sessionId Session ID of the simulator
187 | * @param startX Initial X coordinate
188 | * @param startY Initial Y coordinate
189 | * @param endX Final X coordinate
190 | * @param endY Final Y coordinate
191 | * @param duration Swipe duration in milliseconds (optional)
192 | * @param delta Size of each swipe step (optional)
193 | */
194 | swipe(
195 | sessionId: string,
196 | startX: number,
197 | startY: number,
198 | endX: number,
199 | endY: number,
200 | duration?: number,
201 | delta?: number
202 | ): Promise<void>;
203 |
204 | /**
205 | * Presses a device button
206 | * @param sessionId Session ID of the simulator
207 | * @param button Type of button to press
208 | * @param duration Press duration in milliseconds (optional)
209 | */
210 | pressButton?(
211 | sessionId: string,
212 | button: ButtonType,
213 | duration?: number
214 | ): Promise<void>;
215 |
216 | /**
217 | * Inputs text into the simulator
218 | * @param sessionId Session ID of the simulator
219 | * @param text Text to input
220 | */
221 | inputText?(sessionId: string, text: string): Promise<void>;
222 |
223 | /**
224 | * Presses a specific key by its code
225 | * @param sessionId Session ID of the simulator
226 | * @param keyCode Key code
227 | * @param duration Press duration in milliseconds (optional)
228 | */
229 | pressKey?(
230 | sessionId: string,
231 | keyCode: number,
232 | duration?: number
233 | ): Promise<void>;
234 |
235 | /**
236 | * Presses a sequence of keys
237 | * @param sessionId Session ID of the simulator
238 | * @param keyCodes List of key codes to press sequentially
239 | */
240 | pressKeySequence?(
241 | sessionId: string,
242 | keyCodes: number[]
243 | ): Promise<void>;
244 |
245 | // === Accessibility ===
246 |
247 | /**
248 | * Describes all accessibility elements on the screen
249 | * @param sessionId Session ID of the simulator
250 | * @returns Accessibility information for all elements
251 | */
252 | describeAllElements?(sessionId: string): Promise<AccessibilityInfo[]>;
253 |
254 | /**
255 | * Describes the accessibility element at a specific point
256 | * @param sessionId Session ID of the simulator
257 | * @param x X coordinate
258 | * @param y Y coordinate
259 | * @returns Accessibility information for the element at the point
260 | */
261 | describePointElement?(
262 | sessionId: string,
263 | x: number,
264 | y: number
265 | ): Promise<AccessibilityInfo | null>;
266 |
267 | // === Screenshots and Logs ===
268 |
269 | /**
270 | * Takes a screenshot of the simulator
271 | * @param sessionId Session ID of the simulator
272 | * @param outputPath Path where to save the screenshot (optional)
273 | * @returns Buffer with the image or path to the saved file
274 | */
275 | takeScreenshot(sessionId: string, outputPath?: string): Promise<any | string>;
276 |
277 | /**
278 | * Starts video recording of the simulator
279 | * @param sessionId Session ID of the simulator
280 | * @param outputPath Path where to save the video
281 | * @returns Recording ID
282 | */
283 | startVideoRecording?(sessionId: string, outputPath: string): Promise<string>;
284 |
285 | /**
286 | * Stops video recording of the simulator
287 | * @param sessionId Session ID of the simulator
288 | * @param recordingId Recording ID to stop
289 | */
290 | stopVideoRecording?(sessionId: string, recordingId: string): Promise<void>;
291 |
292 | /**
293 | * Gets simulator system logs
294 | * @param sessionId Session ID of the simulator
295 | * @param options Filtering options (optional)
296 | * @returns System logs
297 | */
298 | getSystemLogs(sessionId: string, options?: {
299 | bundle?: string;
300 | since?: Date;
301 | limit?: number;
302 | }): Promise<string>;
303 |
304 | /**
305 | * Gets logs for a specific application
306 | * @param sessionId Session ID of the simulator
307 | * @param bundleId Bundle ID of the application
308 | * @returns Application logs
309 | */
310 | getAppLogs(sessionId: string, bundleId: string): Promise<string>;
311 |
312 | // === Debug ===
313 |
314 | /**
315 | * Starts a debug session for an application
316 | * @param sessionId Session ID of the simulator
317 | * @param bundleId Bundle ID of the application to debug
318 | * @returns Connection port for the debugger
319 | */
320 | startDebugServer?(sessionId: string, bundleId: string): Promise<number>;
321 |
322 | /**
323 | * Stops a debug session
324 | * @param sessionId Session ID of the simulator
325 | */
326 | stopDebugServer?(sessionId: string): Promise<void>;
327 |
328 | /**
329 | * Gets the debug session status
330 | * @param sessionId Session ID of the simulator
331 | * @returns Information about the debug session
332 | */
333 | getDebugServerStatus?(sessionId: string): Promise<{
334 | running: boolean;
335 | port?: number;
336 | bundleId?: string;
337 | }>;
338 |
339 | // === Crash Logs ===
340 |
341 | /**
342 | * Lists available crash logs
343 | * @param sessionId Session ID of the simulator
344 | * @param options Filtering options (optional)
345 | * @returns List of crash log information
346 | */
347 | listCrashLogs?(sessionId: string, options?: {
348 | bundleId?: string;
349 | before?: Date;
350 | since?: Date;
351 | }): Promise<CrashLogInfo[]>;
352 |
353 | /**
354 | * Gets the content of a crash log
355 | * @param sessionId Session ID of the simulator
356 | * @param crashName Name of the crash log
357 | * @returns Content of the crash log
358 | */
359 | getCrashLog?(sessionId: string, crashName: string): Promise<string>;
360 |
361 | /**
362 | * Deletes crash logs
363 | * @param sessionId Session ID of the simulator
364 | * @param options Options for deleting logs (optional)
365 | */
366 | deleteCrashLogs?(sessionId: string, options: {
367 | crashNames?: string[];
368 | bundleId?: string;
369 | before?: Date;
370 | since?: Date;
371 | all?: boolean;
372 | }): Promise<void>;
373 |
374 | // === Miscellaneous ===
375 |
376 | /**
377 | * Installs a dynamic library (.dylib)
378 | * @param sessionId Session ID of the simulator
379 | * @param dylibPath Path to the .dylib library
380 | */
381 | installDylib?(sessionId: string, dylibPath: string): Promise<void>;
382 |
383 | /**
384 | * Opens a URL in the simulator
385 | * @param sessionId Session ID of the simulator
386 | * @param url URL to open
387 | */
388 | openUrl?(sessionId: string, url: string): Promise<void>;
389 |
390 | /**
391 | * Clears the simulator keychain
392 | * @param sessionId Session ID of the simulator
393 | */
394 | clearKeychain?(sessionId: string): Promise<void>;
395 |
396 | /**
397 | * Sets the simulator location
398 | * @param sessionId Session ID of the simulator
399 | * @param latitude Latitude
400 | * @param longitude Longitude
401 | */
402 | setLocation?(
403 | sessionId: string,
404 | latitude: number,
405 | longitude: number
406 | ): Promise<void>;
407 |
408 | /**
409 | * Adds media files to the simulator camera roll
410 | * @param sessionId Session ID of the simulator
411 | * @param mediaPaths Paths to media files
412 | */
413 | addMedia?(sessionId: string, mediaPaths: string[]): Promise<void>;
414 |
415 | /**
416 | * Approves permissions for an application
417 | * @param sessionId Session ID of the simulator
418 | * @param bundleId Bundle ID of the application
419 | * @param permissions Permissions to approve (photos, camera, contacts, etc.)
420 | */
421 | approvePermissions?(
422 | sessionId: string,
423 | bundleId: string,
424 | permissions: string[]
425 | ): Promise<void>;
426 |
427 | /**
428 | * Updates the simulator contacts database
429 | * @param sessionId Session ID of the simulator
430 | * @param dbPath Path to the contacts database
431 | */
432 | updateContacts?(sessionId: string, dbPath: string): Promise<void>;
433 | }
434 |
```