replicant-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/dist/adapters/adb.d.ts +21 -0
  4. package/dist/adapters/adb.js +75 -0
  5. package/dist/adapters/emulator.d.ts +19 -0
  6. package/dist/adapters/emulator.js +72 -0
  7. package/dist/adapters/gradle.d.ts +20 -0
  8. package/dist/adapters/gradle.js +80 -0
  9. package/dist/adapters/index.d.ts +4 -0
  10. package/dist/adapters/index.js +4 -0
  11. package/dist/adapters/ui-automator.d.ts +23 -0
  12. package/dist/adapters/ui-automator.js +53 -0
  13. package/dist/cli/adb.d.ts +2 -0
  14. package/dist/cli/adb.js +256 -0
  15. package/dist/cli/cache.d.ts +2 -0
  16. package/dist/cli/cache.js +115 -0
  17. package/dist/cli/emulator.d.ts +2 -0
  18. package/dist/cli/emulator.js +181 -0
  19. package/dist/cli/formatter.d.ts +52 -0
  20. package/dist/cli/formatter.js +68 -0
  21. package/dist/cli/gradle.d.ts +2 -0
  22. package/dist/cli/gradle.js +192 -0
  23. package/dist/cli/index.d.ts +6 -0
  24. package/dist/cli/index.js +6 -0
  25. package/dist/cli/ui.d.ts +2 -0
  26. package/dist/cli/ui.js +218 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +14 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +6 -0
  31. package/dist/parsers/adb-output.d.ts +4 -0
  32. package/dist/parsers/adb-output.js +32 -0
  33. package/dist/parsers/emulator-output.d.ts +9 -0
  34. package/dist/parsers/emulator-output.js +33 -0
  35. package/dist/parsers/gradle-output.d.ts +30 -0
  36. package/dist/parsers/gradle-output.js +80 -0
  37. package/dist/parsers/index.d.ts +4 -0
  38. package/dist/parsers/index.js +4 -0
  39. package/dist/parsers/ui-dump.d.ts +27 -0
  40. package/dist/parsers/ui-dump.js +142 -0
  41. package/dist/server.d.ts +15 -0
  42. package/dist/server.js +113 -0
  43. package/dist/services/cache-manager.d.ts +22 -0
  44. package/dist/services/cache-manager.js +90 -0
  45. package/dist/services/device-state.d.ts +9 -0
  46. package/dist/services/device-state.js +26 -0
  47. package/dist/services/index.d.ts +3 -0
  48. package/dist/services/index.js +3 -0
  49. package/dist/services/process-runner.d.ts +15 -0
  50. package/dist/services/process-runner.js +62 -0
  51. package/dist/tools/adb-app.d.ts +38 -0
  52. package/dist/tools/adb-app.js +68 -0
  53. package/dist/tools/adb-device.d.ts +31 -0
  54. package/dist/tools/adb-device.js +71 -0
  55. package/dist/tools/adb-logcat.d.ts +54 -0
  56. package/dist/tools/adb-logcat.js +70 -0
  57. package/dist/tools/adb-shell.d.ts +26 -0
  58. package/dist/tools/adb-shell.js +27 -0
  59. package/dist/tools/cache.d.ts +50 -0
  60. package/dist/tools/cache.js +57 -0
  61. package/dist/tools/emulator-device.d.ts +56 -0
  62. package/dist/tools/emulator-device.js +132 -0
  63. package/dist/tools/gradle-build.d.ts +35 -0
  64. package/dist/tools/gradle-build.js +40 -0
  65. package/dist/tools/gradle-get-details.d.ts +32 -0
  66. package/dist/tools/gradle-get-details.js +72 -0
  67. package/dist/tools/gradle-list.d.ts +30 -0
  68. package/dist/tools/gradle-list.js +55 -0
  69. package/dist/tools/gradle-test.d.ts +34 -0
  70. package/dist/tools/gradle-test.js +40 -0
  71. package/dist/tools/index.d.ts +12 -0
  72. package/dist/tools/index.js +12 -0
  73. package/dist/tools/rtfm.d.ts +26 -0
  74. package/dist/tools/rtfm.js +70 -0
  75. package/dist/tools/ui.d.ts +77 -0
  76. package/dist/tools/ui.js +131 -0
  77. package/dist/types/cache.d.ts +24 -0
  78. package/dist/types/cache.js +14 -0
  79. package/dist/types/device.d.ts +11 -0
  80. package/dist/types/device.js +1 -0
  81. package/dist/types/errors.d.ts +31 -0
  82. package/dist/types/errors.js +43 -0
  83. package/dist/types/index.d.ts +3 -0
  84. package/dist/types/index.js +3 -0
  85. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Archit Joshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,386 @@
1
+ # replicant-mcp
2
+
3
+ **Let AI build, test, and debug your Android apps.**
4
+
5
+ [![CI](https://github.com/thecombatwombat/replicant-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/thecombatwombat/replicant-mcp/actions/workflows/ci.yml)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ replicant-mcp is a [Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI assistants like Claude the ability to interact with your Android development environment. Build APKs, launch emulators, install apps, navigate UIs, and debug crashes—all through natural conversation.
10
+
11
+ ---
12
+
13
+ ## Why replicant-mcp?
14
+
15
+ Android development involves juggling a lot: Gradle builds, emulator management, ADB commands, logcat filtering, UI testing. Each has its own CLI, flags, and quirks.
16
+
17
+ replicant-mcp wraps all of this into a clean interface that AI can understand and use effectively:
18
+
19
+ | Without replicant-mcp | With replicant-mcp |
20
+ |-----------------------|-------------------|
21
+ | "Run `./gradlew assembleDebug`, then `adb install`, then `adb shell am start`..." | "Build and run the app" |
22
+ | Copy-paste logcat output, lose context | AI reads filtered logs directly |
23
+ | Screenshot → describe UI → guess coordinates | AI sees accessibility tree, taps elements by text |
24
+ | 5,000 tokens of raw Gradle output | 50-token summary + details on demand |
25
+
26
+ ---
27
+
28
+ ## Quick Start
29
+
30
+ ### Prerequisites
31
+
32
+ You'll need:
33
+ - **Node.js 18+**
34
+ - **Android SDK** with `adb` and `emulator` in your PATH
35
+ - An Android project with `gradlew` (for build tools)
36
+
37
+ Verify your setup:
38
+ ```bash
39
+ node --version # Should be 18+
40
+ adb --version # Should show Android Debug Bridge version
41
+ emulator -version # Should show Android emulator version
42
+ ```
43
+
44
+ ### Installation
45
+
46
+ ```bash
47
+ # Clone the repo
48
+ git clone https://github.com/thecombatwombat/replicant-mcp.git
49
+ cd replicant-mcp
50
+
51
+ # Install dependencies
52
+ npm install
53
+
54
+ # Build
55
+ npm run build
56
+
57
+ # Verify everything works
58
+ npm test
59
+ ```
60
+
61
+ ### Connect to Claude Desktop
62
+
63
+ Add this to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "replicant": {
69
+ "command": "node",
70
+ "args": ["/absolute/path/to/replicant-mcp/dist/index.js"]
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ Restart Claude Desktop. You should see "replicant" in the MCP servers list.
77
+
78
+ ### Alternative: Claude Code Skill
79
+
80
+ If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (Anthropic's CLI), you can install replicant as a skill instead of an MCP server. This provides shell script wrappers optimized for Claude Code's workflow.
81
+
82
+ **Option 1: Via Plugin Marketplace (Recommended)**
83
+ ```bash
84
+ /plugin marketplace add thecombatwombat/replicant-mcp
85
+ /plugin install replicant-dev@replicant-mcp
86
+ ```
87
+
88
+ **Option 2: Manual Installation**
89
+ ```bash
90
+ # From the replicant-mcp directory
91
+ npm run install-skill
92
+ ```
93
+
94
+ After installation, restart Claude Code to load the skill. The skill provides the same Android development capabilities through shell scripts rather than MCP tools.
95
+
96
+ > **Note:** The Claude Code skill is currently compatible with macOS and Linux only. Windows support is planned for a future release.
97
+
98
+ **Which should you choose?**
99
+ - **MCP Server** — Use with Claude Desktop or any MCP-compatible client
100
+ - **Claude Code Skill** — Use with Claude Code CLI for a shell-native experience
101
+
102
+ Both approaches provide the same core functionality.
103
+
104
+ ---
105
+
106
+ ## What Can It Do?
107
+
108
+ ### Build & Test
109
+
110
+ ```
111
+ You: "Build the debug variant of the app"
112
+
113
+ Claude: I'll build the debug APK.
114
+ [Calls gradle-build with operation: "assembleDebug"]
115
+
116
+ Build successful in 34s
117
+ APK: app/build/outputs/apk/debug/app-debug.apk (12.4 MB)
118
+ 2 warnings (use gradle-get-details for full output)
119
+ ```
120
+
121
+ ```
122
+ You: "Run the unit tests for the login module"
123
+
124
+ Claude: Running unit tests for :feature:login...
125
+ [Calls gradle-test with operation: "unitTest", module: ":feature:login"]
126
+
127
+ 47 tests passed, 2 failed, 0 skipped
128
+
129
+ Failed:
130
+ - LoginViewModelTest.testInvalidEmail
131
+ - LoginRepositoryTest.testNetworkError
132
+
133
+ Want me to show the failure details?
134
+ ```
135
+
136
+ ### Emulator Management
137
+
138
+ ```
139
+ You: "Start up a Pixel 7 emulator"
140
+
141
+ Claude: I'll start the Pixel_7_API_34 emulator.
142
+ [Calls emulator-device with operation: "start", avdName: "Pixel_7_API_34"]
143
+
144
+ Emulator started: emulator-5554
145
+ Waiting for boot... ready!
146
+ ```
147
+
148
+ ### Install & Launch Apps
149
+
150
+ ```
151
+ You: "Install the APK we just built and open the app"
152
+
153
+ Claude: Installing and launching...
154
+ [Calls adb-app with operation: "install"]
155
+ [Calls adb-app with operation: "launch"]
156
+
157
+ App installed and running on emulator-5554
158
+ ```
159
+
160
+ ### Debug with Logcat
161
+
162
+ ```
163
+ You: "The app crashed. What happened?"
164
+
165
+ Claude: Let me check the error logs.
166
+ [Calls adb-logcat with level: "error", lines: 50]
167
+
168
+ Found the crash:
169
+
170
+ java.lang.NullPointerException: Attempt to invoke virtual method
171
+ 'void com.example.User.getName()' on a null object reference
172
+ at com.example.ProfileActivity.onCreate(ProfileActivity.kt:47)
173
+
174
+ Looks like `user` is null when ProfileActivity starts.
175
+ Want me to look at that file?
176
+ ```
177
+
178
+ ### UI Automation (No Screenshots Needed!)
179
+
180
+ ```
181
+ You: "Tap the Login button"
182
+
183
+ Claude: I'll find and tap the Login button.
184
+ [Calls ui with operation: "find", selector: { text: "Login" }]
185
+ [Calls ui with operation: "tap", elementIndex: 0]
186
+
187
+ Tapped "Login" at coordinates (540, 1847)
188
+ ```
189
+
190
+ This works by reading the **accessibility tree**—the same data screen readers use. It's faster, cheaper, and more reliable than screenshot-based approaches.
191
+
192
+ ---
193
+
194
+ ## Tool Reference
195
+
196
+ replicant-mcp provides 12 tools organized into categories:
197
+
198
+ ### Build & Test
199
+ | Tool | Description |
200
+ |------|-------------|
201
+ | `gradle-build` | Build APKs and bundles (`assembleDebug`, `assembleRelease`, `bundle`) |
202
+ | `gradle-test` | Run unit and instrumented tests with filtering |
203
+ | `gradle-list` | List modules, build variants, and tasks |
204
+ | `gradle-get-details` | Fetch full logs/errors from cached build results |
205
+
206
+ ### Emulator
207
+ | Tool | Description |
208
+ |------|-------------|
209
+ | `emulator-device` | Create, start, stop emulators; manage snapshots |
210
+
211
+ ### ADB
212
+ | Tool | Description |
213
+ |------|-------------|
214
+ | `adb-device` | List devices, select active device, get properties |
215
+ | `adb-app` | Install, uninstall, launch, stop apps; clear data |
216
+ | `adb-logcat` | Read filtered device logs by package/tag/level |
217
+ | `adb-shell` | Run shell commands (with safety guards) |
218
+
219
+ ### UI Automation
220
+ | Tool | Description |
221
+ |------|-------------|
222
+ | `ui` | Dump accessibility tree, find elements, tap, input text, screenshot |
223
+
224
+ ### Utilities
225
+ | Tool | Description |
226
+ |------|-------------|
227
+ | `cache` | Manage cached outputs (stats, clear, config) |
228
+ | `rtfm` | On-demand documentation for tools |
229
+
230
+ **Want details?** Ask Claude to call `rtfm` with a category like "build", "adb", "emulator", or "ui".
231
+
232
+ ---
233
+
234
+ ## Design Philosophy
235
+
236
+ ### Progressive Disclosure
237
+
238
+ Gradle builds can produce thousands of lines of output. Dumping all of that into an AI context is wasteful and confusing.
239
+
240
+ Instead, replicant-mcp returns **summaries with cache IDs**:
241
+
242
+ ```json
243
+ {
244
+ "buildId": "build-a1b2c3-1705789200",
245
+ "summary": {
246
+ "success": true,
247
+ "duration": "34s",
248
+ "apkSize": "12.4 MB",
249
+ "warnings": 2
250
+ }
251
+ }
252
+ ```
253
+
254
+ If the AI needs the full output (e.g., to debug a failure), it can request it:
255
+
256
+ ```json
257
+ { "tool": "gradle-get-details", "id": "build-a1b2c3-1705789200", "detailType": "errors" }
258
+ ```
259
+
260
+ This typically reduces token usage by **90-99%**.
261
+
262
+ ### Accessibility-First UI
263
+
264
+ Most AI-driven UI automation uses screenshots: capture the screen, send it to a vision model, get coordinates, click.
265
+
266
+ replicant-mcp takes a different approach: it reads the **accessibility tree**—the same structured data that powers screen readers. This is:
267
+
268
+ - **Faster** — No image processing
269
+ - **Cheaper** — Text is smaller than images
270
+ - **More reliable** — Elements are identified by text/ID, not pixel coordinates
271
+ - **Better for apps** — Encourages accessible app development
272
+
273
+ ### Single Device Focus
274
+
275
+ Instead of passing `deviceId` to every command, you select a device once:
276
+
277
+ ```json
278
+ { "tool": "adb-device", "operation": "select", "deviceId": "emulator-5554" }
279
+ ```
280
+
281
+ All subsequent commands target that device automatically. Simple.
282
+
283
+ ### Safety Guards
284
+
285
+ The `adb-shell` tool blocks dangerous commands like `rm -rf /`, `reboot`, and `su`. You can run shell commands, but not brick your device.
286
+
287
+ ---
288
+
289
+ ## Development
290
+
291
+ ### Project Structure
292
+
293
+ ```
294
+ src/
295
+ index.ts # Entry point
296
+ server.ts # MCP server setup
297
+ tools/ # Tool implementations (one file per tool)
298
+ adapters/ # CLI wrappers (adb, emulator, gradle)
299
+ services/ # Core services (cache, device state, process runner)
300
+ parsers/ # Output parsers
301
+ types/ # TypeScript types
302
+ docs/rtfm/ # On-demand documentation
303
+ tests/ # Unit and integration tests
304
+ scripts/ # Utility scripts
305
+ ```
306
+
307
+ ### Running Tests
308
+
309
+ ```bash
310
+ # All tests
311
+ npm test
312
+
313
+ # Unit tests only
314
+ npm run test:unit
315
+
316
+ # Integration tests (MCP protocol compliance)
317
+ npm run test:integration
318
+
319
+ # With coverage
320
+ npm run test:coverage
321
+
322
+ # Full validation (build + all tests)
323
+ npm run validate
324
+ ```
325
+
326
+ ### Checking Prerequisites
327
+
328
+ ```bash
329
+ npm run check-prereqs
330
+ ```
331
+
332
+ This verifies your Android SDK setup and reports what's available.
333
+
334
+ ---
335
+
336
+ ## Troubleshooting
337
+
338
+ ### "No device selected"
339
+
340
+ Run `adb-device` with `operation: "list"` to see available devices, then `operation: "select"` to choose one. If only one device is connected, it's auto-selected.
341
+
342
+ ### "Gradle wrapper not found"
343
+
344
+ Make sure you're in an Android project directory that contains `gradlew`. The Gradle tools won't work from other locations.
345
+
346
+ ### "Command timed out"
347
+
348
+ Long-running operations (builds, tests) have a 5-minute default timeout. If your builds are slower, you may need to adjust the timeout in the adapter.
349
+
350
+ ### Emulator won't start
351
+
352
+ Check that:
353
+ 1. You have an AVD created (`avdmanager list avd`)
354
+ 2. Virtualization is enabled (KVM on Linux, HAXM on Mac/Windows)
355
+ 3. Enough disk space for the emulator
356
+
357
+ ---
358
+
359
+ ## Contributing
360
+
361
+ Contributions are welcome! Please:
362
+
363
+ 1. Fork the repo
364
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
365
+ 3. Make your changes
366
+ 4. Run `npm run validate` to ensure tests pass
367
+ 5. Commit with a descriptive message
368
+ 6. Push and open a PR
369
+
370
+ ---
371
+
372
+ ## Acknowledgments
373
+
374
+ - Inspired by [xc-mcp](https://github.com/conorluddy/xc-mcp) for iOS
375
+ - Built on the [Model Context Protocol](https://modelcontextprotocol.io/)
376
+ - Thanks to the Android team for `adb` and the emulator
377
+
378
+ ---
379
+
380
+ ## License
381
+
382
+ [MIT](LICENSE)
383
+
384
+ ---
385
+
386
+ **Questions? Issues? Ideas?** [Open an issue](https://github.com/thecombatwombat/replicant-mcp/issues) — we'd love to hear from you.
@@ -0,0 +1,21 @@
1
+ import { ProcessRunner, RunResult } from "../services/index.js";
2
+ import { Device } from "../types/index.js";
3
+ export declare class AdbAdapter {
4
+ private runner;
5
+ constructor(runner?: ProcessRunner);
6
+ getDevices(): Promise<Device[]>;
7
+ getPackages(deviceId: string): Promise<string[]>;
8
+ install(deviceId: string, apkPath: string): Promise<void>;
9
+ uninstall(deviceId: string, packageName: string): Promise<void>;
10
+ launch(deviceId: string, packageName: string): Promise<void>;
11
+ stop(deviceId: string, packageName: string): Promise<void>;
12
+ clearData(deviceId: string, packageName: string): Promise<void>;
13
+ shell(deviceId: string, command: string, timeoutMs?: number): Promise<RunResult>;
14
+ logcat(deviceId: string, options: {
15
+ lines?: number;
16
+ filter?: string;
17
+ }): Promise<string>;
18
+ waitForDevice(deviceId: string, timeoutMs?: number): Promise<void>;
19
+ getProperties(deviceId: string): Promise<Record<string, string>>;
20
+ private adb;
21
+ }
@@ -0,0 +1,75 @@
1
+ import { ProcessRunner } from "../services/index.js";
2
+ import { ReplicantError, ErrorCode } from "../types/index.js";
3
+ import { parseDeviceList, parsePackageList } from "../parsers/adb-output.js";
4
+ export class AdbAdapter {
5
+ runner;
6
+ constructor(runner = new ProcessRunner()) {
7
+ this.runner = runner;
8
+ }
9
+ async getDevices() {
10
+ const result = await this.adb(["devices"]);
11
+ return parseDeviceList(result.stdout);
12
+ }
13
+ async getPackages(deviceId) {
14
+ const result = await this.adb(["-s", deviceId, "shell", "pm", "list", "packages"]);
15
+ return parsePackageList(result.stdout);
16
+ }
17
+ async install(deviceId, apkPath) {
18
+ const result = await this.adb(["-s", deviceId, "install", "-r", apkPath]);
19
+ if (result.exitCode !== 0 || result.stdout.includes("Failure")) {
20
+ throw new ReplicantError(ErrorCode.INSTALL_FAILED, `Failed to install APK: ${result.stdout}`, "Check the APK path and device state");
21
+ }
22
+ }
23
+ async uninstall(deviceId, packageName) {
24
+ const result = await this.adb(["-s", deviceId, "uninstall", packageName]);
25
+ if (result.exitCode !== 0) {
26
+ throw new ReplicantError(ErrorCode.PACKAGE_NOT_FOUND, `Failed to uninstall ${packageName}`, "Check the package name");
27
+ }
28
+ }
29
+ async launch(deviceId, packageName) {
30
+ // Get the main activity using dumpsys
31
+ const result = await this.adb([
32
+ "-s", deviceId, "shell", "monkey",
33
+ "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"
34
+ ]);
35
+ if (result.exitCode !== 0) {
36
+ throw new ReplicantError(ErrorCode.PACKAGE_NOT_FOUND, `Failed to launch ${packageName}`, "Check the package name and ensure the app is installed");
37
+ }
38
+ }
39
+ async stop(deviceId, packageName) {
40
+ await this.adb(["-s", deviceId, "shell", "am", "force-stop", packageName]);
41
+ }
42
+ async clearData(deviceId, packageName) {
43
+ await this.adb(["-s", deviceId, "shell", "pm", "clear", packageName]);
44
+ }
45
+ async shell(deviceId, command, timeoutMs) {
46
+ return this.adb(["-s", deviceId, "shell", command], timeoutMs);
47
+ }
48
+ async logcat(deviceId, options) {
49
+ const args = ["-s", deviceId, "logcat", "-d"];
50
+ if (options.lines) {
51
+ args.push("-t", options.lines.toString());
52
+ }
53
+ if (options.filter) {
54
+ args.push(...options.filter.split(" "));
55
+ }
56
+ const result = await this.adb(args);
57
+ return result.stdout;
58
+ }
59
+ async waitForDevice(deviceId, timeoutMs = 30000) {
60
+ await this.adb(["-s", deviceId, "wait-for-device"], timeoutMs);
61
+ }
62
+ async getProperties(deviceId) {
63
+ const result = await this.adb(["-s", deviceId, "shell", "getprop"]);
64
+ const props = {};
65
+ const regex = /\[([^\]]+)\]:\s*\[([^\]]*)\]/g;
66
+ let match;
67
+ while ((match = regex.exec(result.stdout)) !== null) {
68
+ props[match[1]] = match[2];
69
+ }
70
+ return props;
71
+ }
72
+ async adb(args, timeoutMs) {
73
+ return this.runner.run("adb", args, { timeoutMs });
74
+ }
75
+ }
@@ -0,0 +1,19 @@
1
+ import { ProcessRunner } from "../services/index.js";
2
+ import { AvdInfo } from "../parsers/emulator-output.js";
3
+ export interface EmulatorListResult {
4
+ available: AvdInfo[];
5
+ running: string[];
6
+ }
7
+ export declare class EmulatorAdapter {
8
+ private runner;
9
+ constructor(runner?: ProcessRunner);
10
+ list(): Promise<EmulatorListResult>;
11
+ create(name: string, device: string, systemImage: string): Promise<void>;
12
+ start(avdName: string): Promise<string>;
13
+ kill(emulatorId: string): Promise<void>;
14
+ wipe(avdName: string): Promise<void>;
15
+ snapshotSave(emulatorId: string, name: string): Promise<void>;
16
+ snapshotLoad(emulatorId: string, name: string): Promise<void>;
17
+ snapshotList(emulatorId: string): Promise<string[]>;
18
+ snapshotDelete(emulatorId: string, name: string): Promise<void>;
19
+ }
@@ -0,0 +1,72 @@
1
+ import { ProcessRunner } from "../services/index.js";
2
+ import { ReplicantError, ErrorCode } from "../types/index.js";
3
+ import { parseAvdList, parseEmulatorList, parseSnapshotList } from "../parsers/emulator-output.js";
4
+ export class EmulatorAdapter {
5
+ runner;
6
+ constructor(runner = new ProcessRunner()) {
7
+ this.runner = runner;
8
+ }
9
+ async list() {
10
+ const [avdResult, runningResult] = await Promise.all([
11
+ this.runner.run("avdmanager", ["list", "avd"]),
12
+ this.runner.run("emulator", ["-list-avds"]),
13
+ ]);
14
+ return {
15
+ available: parseAvdList(avdResult.stdout),
16
+ running: parseEmulatorList(runningResult.stdout),
17
+ };
18
+ }
19
+ async create(name, device, systemImage) {
20
+ const result = await this.runner.run("avdmanager", [
21
+ "create", "avd",
22
+ "-n", name,
23
+ "-k", systemImage,
24
+ "-d", device,
25
+ "--force",
26
+ ], { timeoutMs: 60000 });
27
+ if (result.exitCode !== 0) {
28
+ throw new ReplicantError(ErrorCode.EMULATOR_START_FAILED, `Failed to create AVD: ${result.stderr}`, "Check device and system image names");
29
+ }
30
+ }
31
+ async start(avdName) {
32
+ // Start emulator in background - don't wait for it
33
+ // Returns immediately, emulator boots in background
34
+ this.runner.run("emulator", [
35
+ "-avd", avdName,
36
+ "-no-snapshot-load",
37
+ "-no-boot-anim",
38
+ ], { timeoutMs: 5000 }).catch(() => {
39
+ // Expected to "timeout" as emulator runs forever
40
+ });
41
+ // Give it a moment to register
42
+ await new Promise((r) => setTimeout(r, 2000));
43
+ // Find the new emulator ID
44
+ const result = await this.runner.run("adb", ["devices"]);
45
+ const match = result.stdout.match(/emulator-\d+/);
46
+ if (!match) {
47
+ throw new ReplicantError(ErrorCode.EMULATOR_START_FAILED, `Emulator ${avdName} failed to start`, "Check the AVD name and try again");
48
+ }
49
+ return match[0];
50
+ }
51
+ async kill(emulatorId) {
52
+ await this.runner.run("adb", ["-s", emulatorId, "emu", "kill"]);
53
+ }
54
+ async wipe(avdName) {
55
+ await this.runner.run("emulator", ["-avd", avdName, "-wipe-data", "-no-window"], { timeoutMs: 5000 }).catch(() => {
56
+ // Expected behavior
57
+ });
58
+ }
59
+ async snapshotSave(emulatorId, name) {
60
+ await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "save", name]);
61
+ }
62
+ async snapshotLoad(emulatorId, name) {
63
+ await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "load", name]);
64
+ }
65
+ async snapshotList(emulatorId) {
66
+ const result = await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "list"]);
67
+ return parseSnapshotList(result.stdout);
68
+ }
69
+ async snapshotDelete(emulatorId, name) {
70
+ await this.runner.run("adb", ["-s", emulatorId, "emu", "avd", "snapshot", "delete", name]);
71
+ }
72
+ }
@@ -0,0 +1,20 @@
1
+ import { ProcessRunner } from "../services/index.js";
2
+ import { BuildResult, TestResult, VariantInfo } from "../parsers/gradle-output.js";
3
+ export declare class GradleAdapter {
4
+ private runner;
5
+ private projectPath?;
6
+ constructor(runner?: ProcessRunner, projectPath?: string | undefined);
7
+ build(operation: "assembleDebug" | "assembleRelease" | "bundle", module?: string, flavor?: string): Promise<{
8
+ result: BuildResult;
9
+ fullOutput: string;
10
+ }>;
11
+ test(operation: "unitTest" | "connectedTest", module?: string, filter?: string): Promise<{
12
+ result: TestResult;
13
+ fullOutput: string;
14
+ }>;
15
+ listModules(): Promise<string[]>;
16
+ listVariants(module?: string): Promise<VariantInfo[]>;
17
+ listTasks(module?: string): Promise<string[]>;
18
+ clean(stopDaemons?: boolean): Promise<void>;
19
+ private gradle;
20
+ }