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.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/dist/adapters/adb.d.ts +21 -0
- package/dist/adapters/adb.js +75 -0
- package/dist/adapters/emulator.d.ts +19 -0
- package/dist/adapters/emulator.js +72 -0
- package/dist/adapters/gradle.d.ts +20 -0
- package/dist/adapters/gradle.js +80 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/ui-automator.d.ts +23 -0
- package/dist/adapters/ui-automator.js +53 -0
- package/dist/cli/adb.d.ts +2 -0
- package/dist/cli/adb.js +256 -0
- package/dist/cli/cache.d.ts +2 -0
- package/dist/cli/cache.js +115 -0
- package/dist/cli/emulator.d.ts +2 -0
- package/dist/cli/emulator.js +181 -0
- package/dist/cli/formatter.d.ts +52 -0
- package/dist/cli/formatter.js +68 -0
- package/dist/cli/gradle.d.ts +2 -0
- package/dist/cli/gradle.js +192 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/ui.d.ts +2 -0
- package/dist/cli/ui.js +218 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/parsers/adb-output.d.ts +4 -0
- package/dist/parsers/adb-output.js +32 -0
- package/dist/parsers/emulator-output.d.ts +9 -0
- package/dist/parsers/emulator-output.js +33 -0
- package/dist/parsers/gradle-output.d.ts +30 -0
- package/dist/parsers/gradle-output.js +80 -0
- package/dist/parsers/index.d.ts +4 -0
- package/dist/parsers/index.js +4 -0
- package/dist/parsers/ui-dump.d.ts +27 -0
- package/dist/parsers/ui-dump.js +142 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +113 -0
- package/dist/services/cache-manager.d.ts +22 -0
- package/dist/services/cache-manager.js +90 -0
- package/dist/services/device-state.d.ts +9 -0
- package/dist/services/device-state.js +26 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/process-runner.d.ts +15 -0
- package/dist/services/process-runner.js +62 -0
- package/dist/tools/adb-app.d.ts +38 -0
- package/dist/tools/adb-app.js +68 -0
- package/dist/tools/adb-device.d.ts +31 -0
- package/dist/tools/adb-device.js +71 -0
- package/dist/tools/adb-logcat.d.ts +54 -0
- package/dist/tools/adb-logcat.js +70 -0
- package/dist/tools/adb-shell.d.ts +26 -0
- package/dist/tools/adb-shell.js +27 -0
- package/dist/tools/cache.d.ts +50 -0
- package/dist/tools/cache.js +57 -0
- package/dist/tools/emulator-device.d.ts +56 -0
- package/dist/tools/emulator-device.js +132 -0
- package/dist/tools/gradle-build.d.ts +35 -0
- package/dist/tools/gradle-build.js +40 -0
- package/dist/tools/gradle-get-details.d.ts +32 -0
- package/dist/tools/gradle-get-details.js +72 -0
- package/dist/tools/gradle-list.d.ts +30 -0
- package/dist/tools/gradle-list.js +55 -0
- package/dist/tools/gradle-test.d.ts +34 -0
- package/dist/tools/gradle-test.js +40 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/rtfm.d.ts +26 -0
- package/dist/tools/rtfm.js +70 -0
- package/dist/tools/ui.d.ts +77 -0
- package/dist/tools/ui.js +131 -0
- package/dist/types/cache.d.ts +24 -0
- package/dist/types/cache.js +14 -0
- package/dist/types/device.d.ts +11 -0
- package/dist/types/device.js +1 -0
- package/dist/types/errors.d.ts +31 -0
- package/dist/types/errors.js +43 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/package.json +64 -0
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { UiAutomatorAdapter } from "../adapters/index.js";
|
|
3
|
+
import { DeviceStateManager, CacheManager } from "../services/index.js";
|
|
4
|
+
import { formatUiDump } from "./formatter.js";
|
|
5
|
+
import { CACHE_TTLS } from "../types/index.js";
|
|
6
|
+
const adapter = new UiAutomatorAdapter();
|
|
7
|
+
const deviceState = new DeviceStateManager();
|
|
8
|
+
const cache = new CacheManager();
|
|
9
|
+
// Store last find results for tap --index
|
|
10
|
+
let lastFindResults = [];
|
|
11
|
+
export function createUiCommand() {
|
|
12
|
+
const ui = new Command("ui").description("UI automation and accessibility inspection");
|
|
13
|
+
// Dump subcommand
|
|
14
|
+
ui.command("dump")
|
|
15
|
+
.description("Dump the accessibility tree")
|
|
16
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
17
|
+
.option("--json", "Output as JSON")
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const deviceId = options.device || getDeviceId();
|
|
21
|
+
const tree = await adapter.dump(deviceId);
|
|
22
|
+
// Cache the dump
|
|
23
|
+
const cacheId = cache.generateId("ui-dump");
|
|
24
|
+
cache.set(cacheId, { tree }, "ui-dump", CACHE_TTLS.UI_TREE);
|
|
25
|
+
if (options.json) {
|
|
26
|
+
console.log(JSON.stringify({ tree, cacheId }, null, 2));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Convert tree to UiElements for formatting
|
|
30
|
+
const elements = treeToElements(tree);
|
|
31
|
+
const screenName = extractScreenName(tree);
|
|
32
|
+
console.log(formatUiDump({
|
|
33
|
+
screenName,
|
|
34
|
+
elements,
|
|
35
|
+
}));
|
|
36
|
+
console.log(`Cache ID: ${cacheId}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
41
|
+
console.error(`Error: ${errorMessage}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// Find subcommand
|
|
46
|
+
ui.command("find")
|
|
47
|
+
.description("Find UI elements by selector")
|
|
48
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
49
|
+
.option("-t, --text <text>", "Exact text match")
|
|
50
|
+
.option("-c, --contains <text>", "Text contains match")
|
|
51
|
+
.option("-i, --id <resourceId>", "Resource ID match")
|
|
52
|
+
.option("--class <className>", "Class name match")
|
|
53
|
+
.option("--json", "Output as JSON")
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
try {
|
|
56
|
+
const deviceId = options.device || getDeviceId();
|
|
57
|
+
const selector = {};
|
|
58
|
+
if (options.text)
|
|
59
|
+
selector.text = options.text;
|
|
60
|
+
if (options.contains)
|
|
61
|
+
selector.textContains = options.contains;
|
|
62
|
+
if (options.id)
|
|
63
|
+
selector.resourceId = options.id;
|
|
64
|
+
if (options.class)
|
|
65
|
+
selector.className = options.class;
|
|
66
|
+
// Ensure at least one selector is provided
|
|
67
|
+
if (Object.keys(selector).length === 0) {
|
|
68
|
+
console.error("Error: At least one selector option required (--text, --contains, --id, or --class)");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const results = await adapter.find(deviceId, selector);
|
|
72
|
+
lastFindResults = results;
|
|
73
|
+
// Cache the results
|
|
74
|
+
const cacheId = cache.generateId("ui-find");
|
|
75
|
+
cache.set(cacheId, { results, selector }, "ui-find", CACHE_TTLS.UI_TREE);
|
|
76
|
+
if (options.json) {
|
|
77
|
+
console.log(JSON.stringify({ results, count: results.length, cacheId }, null, 2));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (results.length === 0) {
|
|
81
|
+
console.log("No elements found matching selector");
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(`Found ${results.length} element(s):`);
|
|
85
|
+
results.forEach((node, idx) => {
|
|
86
|
+
const desc = node.text || node.contentDesc || node.resourceId || node.className;
|
|
87
|
+
console.log(` [${idx}] ${node.className.split(".").pop()} "${desc}" at (${node.centerX}, ${node.centerY})`);
|
|
88
|
+
});
|
|
89
|
+
console.log(`\nUse 'ui tap --index <n>' to tap an element`);
|
|
90
|
+
}
|
|
91
|
+
console.log(`Cache ID: ${cacheId}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
96
|
+
console.error(`Error: ${errorMessage}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// Tap subcommand
|
|
101
|
+
ui.command("tap")
|
|
102
|
+
.description("Tap a UI element or coordinates")
|
|
103
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
104
|
+
.option("-i, --index <index>", "Index from last find results")
|
|
105
|
+
.option("-x, --x <x>", "X coordinate")
|
|
106
|
+
.option("-y, --y <y>", "Y coordinate")
|
|
107
|
+
.action(async (options) => {
|
|
108
|
+
try {
|
|
109
|
+
const deviceId = options.device || getDeviceId();
|
|
110
|
+
if (options.index !== undefined) {
|
|
111
|
+
const index = parseInt(options.index, 10);
|
|
112
|
+
if (lastFindResults.length === 0) {
|
|
113
|
+
console.error("Error: No previous find results. Run 'ui find' first.");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (index < 0 || index >= lastFindResults.length) {
|
|
117
|
+
console.error(`Error: Index ${index} out of range (0-${lastFindResults.length - 1})`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const element = lastFindResults[index];
|
|
121
|
+
await adapter.tapElement(deviceId, element);
|
|
122
|
+
console.log(`Tapped element [${index}] at (${element.centerX}, ${element.centerY})`);
|
|
123
|
+
}
|
|
124
|
+
else if (options.x !== undefined && options.y !== undefined) {
|
|
125
|
+
const x = parseInt(options.x, 10);
|
|
126
|
+
const y = parseInt(options.y, 10);
|
|
127
|
+
await adapter.tap(deviceId, x, y);
|
|
128
|
+
console.log(`Tapped at (${x}, ${y})`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.error("Error: Provide either --index or both --x and --y");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
137
|
+
console.error(`Error: ${errorMessage}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// Input subcommand
|
|
142
|
+
ui.command("input <text>")
|
|
143
|
+
.description("Input text to the focused element")
|
|
144
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
145
|
+
.action(async (text, options) => {
|
|
146
|
+
try {
|
|
147
|
+
const deviceId = options.device || getDeviceId();
|
|
148
|
+
await adapter.input(deviceId, text);
|
|
149
|
+
console.log(`Input text: "${text}"`);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
153
|
+
console.error(`Error: ${errorMessage}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// Screenshot subcommand
|
|
158
|
+
ui.command("screenshot [path]")
|
|
159
|
+
.description("Take a screenshot")
|
|
160
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
161
|
+
.action(async (path, options) => {
|
|
162
|
+
try {
|
|
163
|
+
const deviceId = options.device || getDeviceId();
|
|
164
|
+
const outputPath = path || `screenshot-${Date.now()}.png`;
|
|
165
|
+
await adapter.screenshot(deviceId, outputPath);
|
|
166
|
+
console.log(`Screenshot saved: ${outputPath}`);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
170
|
+
console.error(`Error: ${errorMessage}`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return ui;
|
|
175
|
+
}
|
|
176
|
+
function getDeviceId() {
|
|
177
|
+
const device = deviceState.requireCurrentDevice();
|
|
178
|
+
return device.id;
|
|
179
|
+
}
|
|
180
|
+
function extractScreenName(tree) {
|
|
181
|
+
// Try to find an activity or window name from the tree
|
|
182
|
+
if (tree.length > 0) {
|
|
183
|
+
const root = tree[0];
|
|
184
|
+
// Common patterns for activity names in class names
|
|
185
|
+
if (root.className.includes("Activity")) {
|
|
186
|
+
return root.className.split(".").pop() || "Unknown";
|
|
187
|
+
}
|
|
188
|
+
if (root.className.includes("DecorView")) {
|
|
189
|
+
return "Main Window";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return "Unknown Screen";
|
|
193
|
+
}
|
|
194
|
+
function treeToElements(tree) {
|
|
195
|
+
const elements = [];
|
|
196
|
+
let elementIndex = 0;
|
|
197
|
+
function walk(node) {
|
|
198
|
+
// Only include interactive or text-containing elements
|
|
199
|
+
if (node.clickable || node.focusable || node.text || node.contentDesc) {
|
|
200
|
+
elements.push({
|
|
201
|
+
index: elementIndex++,
|
|
202
|
+
type: node.className.split(".").pop() || "Unknown",
|
|
203
|
+
text: node.text || undefined,
|
|
204
|
+
hint: node.contentDesc || undefined,
|
|
205
|
+
focused: node.focusable,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (node.children) {
|
|
209
|
+
for (const child of node.children) {
|
|
210
|
+
walk(child);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const node of tree) {
|
|
215
|
+
walk(node);
|
|
216
|
+
}
|
|
217
|
+
return elements;
|
|
218
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { createGradleCommand, createAdbCommand, createEmulatorCommand, createUiCommand, createCacheCommand, } from "./cli/index.js";
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name("replicant")
|
|
7
|
+
.description("Android development CLI for Claude Code skills")
|
|
8
|
+
.version("1.0.0");
|
|
9
|
+
program.addCommand(createGradleCommand());
|
|
10
|
+
program.addCommand(createAdbCommand());
|
|
11
|
+
program.addCommand(createEmulatorCommand());
|
|
12
|
+
program.addCommand(createUiCommand());
|
|
13
|
+
program.addCommand(createCacheCommand());
|
|
14
|
+
program.parse();
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function parseDeviceList(output) {
|
|
2
|
+
const lines = output.split("\n").slice(1); // Skip header
|
|
3
|
+
const devices = [];
|
|
4
|
+
for (const line of lines) {
|
|
5
|
+
const trimmed = line.trim();
|
|
6
|
+
if (!trimmed)
|
|
7
|
+
continue;
|
|
8
|
+
const [id, statusStr] = trimmed.split(/\s+/);
|
|
9
|
+
if (!id)
|
|
10
|
+
continue;
|
|
11
|
+
const type = id.startsWith("emulator") ? "emulator" : "physical";
|
|
12
|
+
const status = statusStr === "device" ? "online" : "offline";
|
|
13
|
+
devices.push({
|
|
14
|
+
id,
|
|
15
|
+
type,
|
|
16
|
+
name: id, // Can be enriched later with getprop
|
|
17
|
+
status,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return devices;
|
|
21
|
+
}
|
|
22
|
+
export function parsePackageList(output) {
|
|
23
|
+
return output
|
|
24
|
+
.split("\n")
|
|
25
|
+
.filter((line) => line.startsWith("package:"))
|
|
26
|
+
.map((line) => line.replace("package:", "").trim());
|
|
27
|
+
}
|
|
28
|
+
export function parseGetProp(output, prop) {
|
|
29
|
+
const regex = new RegExp(`\\[${prop}\\]:\\s*\\[(.*)\\]`);
|
|
30
|
+
const match = output.match(regex);
|
|
31
|
+
return match?.[1];
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface AvdInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
path?: string;
|
|
4
|
+
target?: string;
|
|
5
|
+
skin?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseAvdList(output: string): AvdInfo[];
|
|
8
|
+
export declare function parseEmulatorList(output: string): string[];
|
|
9
|
+
export declare function parseSnapshotList(output: string): string[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function parseAvdList(output) {
|
|
2
|
+
const avds = [];
|
|
3
|
+
const blocks = output.split("---------");
|
|
4
|
+
for (const block of blocks) {
|
|
5
|
+
const nameMatch = block.match(/Name:\s*(.+)/);
|
|
6
|
+
if (!nameMatch)
|
|
7
|
+
continue;
|
|
8
|
+
const pathMatch = block.match(/Path:\s*(.+)/);
|
|
9
|
+
const targetMatch = block.match(/Target:\s*(.+)/);
|
|
10
|
+
const skinMatch = block.match(/Skin:\s*(.+)/);
|
|
11
|
+
avds.push({
|
|
12
|
+
name: nameMatch[1].trim(),
|
|
13
|
+
path: pathMatch?.[1].trim(),
|
|
14
|
+
target: targetMatch?.[1].trim(),
|
|
15
|
+
skin: skinMatch?.[1].trim(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return avds;
|
|
19
|
+
}
|
|
20
|
+
export function parseEmulatorList(output) {
|
|
21
|
+
return output
|
|
22
|
+
.split("\n")
|
|
23
|
+
.map((line) => line.trim())
|
|
24
|
+
.filter((line) => line.startsWith("emulator-"));
|
|
25
|
+
}
|
|
26
|
+
export function parseSnapshotList(output) {
|
|
27
|
+
// Output format: "snapshot_name size date"
|
|
28
|
+
return output
|
|
29
|
+
.split("\n")
|
|
30
|
+
.slice(1) // Skip header
|
|
31
|
+
.map((line) => line.split(/\s+/)[0])
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface BuildResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
duration?: string;
|
|
4
|
+
tasksExecuted?: number;
|
|
5
|
+
warnings: number;
|
|
6
|
+
errors: number;
|
|
7
|
+
failedTask?: string;
|
|
8
|
+
apkPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TestResult {
|
|
11
|
+
passed: number;
|
|
12
|
+
failed: number;
|
|
13
|
+
skipped: number;
|
|
14
|
+
total: number;
|
|
15
|
+
duration?: string;
|
|
16
|
+
failures: Array<{
|
|
17
|
+
test: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseBuildOutput(output: string): BuildResult;
|
|
22
|
+
export declare function parseTestOutput(output: string): TestResult;
|
|
23
|
+
export declare function parseModuleList(output: string): string[];
|
|
24
|
+
export interface VariantInfo {
|
|
25
|
+
name: string;
|
|
26
|
+
buildType: string;
|
|
27
|
+
flavors: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function parseVariantList(output: string): VariantInfo[];
|
|
30
|
+
export declare function parseTaskList(output: string): string[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export function parseBuildOutput(output) {
|
|
2
|
+
const success = output.includes("BUILD SUCCESSFUL");
|
|
3
|
+
const durationMatch = output.match(/BUILD (?:SUCCESSFUL|FAILED) in (\S+)/);
|
|
4
|
+
const tasksMatch = output.match(/(\d+) actionable tasks/);
|
|
5
|
+
const failedTaskMatch = output.match(/Task (:\S+) FAILED/);
|
|
6
|
+
const warnings = (output.match(/^w:/gm) || []).length;
|
|
7
|
+
const errors = (output.match(/^e:/gm) || []).length;
|
|
8
|
+
// Try to find APK path
|
|
9
|
+
const apkMatch = output.match(/(\S+\.apk)/);
|
|
10
|
+
return {
|
|
11
|
+
success,
|
|
12
|
+
duration: durationMatch?.[1],
|
|
13
|
+
tasksExecuted: tasksMatch ? parseInt(tasksMatch[1], 10) : undefined,
|
|
14
|
+
warnings,
|
|
15
|
+
errors,
|
|
16
|
+
failedTask: failedTaskMatch?.[1],
|
|
17
|
+
apkPath: apkMatch?.[1],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function parseTestOutput(output) {
|
|
21
|
+
const summaryMatch = output.match(/(\d+) tests? completed(?:, (\d+) failed)?(?:, (\d+) skipped)?/);
|
|
22
|
+
const total = summaryMatch ? parseInt(summaryMatch[1], 10) : 0;
|
|
23
|
+
const failed = summaryMatch?.[2] ? parseInt(summaryMatch[2], 10) : 0;
|
|
24
|
+
const skipped = summaryMatch?.[3] ? parseInt(summaryMatch[3], 10) : 0;
|
|
25
|
+
const passed = total - failed - skipped;
|
|
26
|
+
const durationMatch = output.match(/in (\S+)/);
|
|
27
|
+
// Extract failure details
|
|
28
|
+
const failures = [];
|
|
29
|
+
const failureRegex = /(\S+) > (\S+) FAILED/g;
|
|
30
|
+
let match;
|
|
31
|
+
while ((match = failureRegex.exec(output)) !== null) {
|
|
32
|
+
failures.push({ test: `${match[1]}.${match[2]}`, message: "" });
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
passed,
|
|
36
|
+
failed,
|
|
37
|
+
skipped,
|
|
38
|
+
total,
|
|
39
|
+
duration: durationMatch?.[1],
|
|
40
|
+
failures,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function parseModuleList(output) {
|
|
44
|
+
const modules = [];
|
|
45
|
+
const regex = /Project '(:\S+)'/g;
|
|
46
|
+
let match;
|
|
47
|
+
while ((match = regex.exec(output)) !== null) {
|
|
48
|
+
modules.push(match[1]);
|
|
49
|
+
}
|
|
50
|
+
return modules;
|
|
51
|
+
}
|
|
52
|
+
export function parseVariantList(output) {
|
|
53
|
+
// This is a simplified parser - actual output varies by project
|
|
54
|
+
const variants = [];
|
|
55
|
+
const lines = output.split("\n");
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed || trimmed.startsWith(">") || trimmed.startsWith("Task"))
|
|
59
|
+
continue;
|
|
60
|
+
// Common pattern: "debug", "release", "freeDebug", "paidRelease"
|
|
61
|
+
if (/^[a-z]+[A-Z]?[a-z]*$/.test(trimmed)) {
|
|
62
|
+
const isDebug = trimmed.toLowerCase().includes("debug");
|
|
63
|
+
const isRelease = trimmed.toLowerCase().includes("release");
|
|
64
|
+
const buildType = isDebug ? "debug" : isRelease ? "release" : trimmed;
|
|
65
|
+
const flavorPart = trimmed.replace(/debug|release/gi, "");
|
|
66
|
+
const flavors = flavorPart ? [flavorPart.toLowerCase()] : [];
|
|
67
|
+
variants.push({ name: trimmed, buildType, flavors });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return variants;
|
|
71
|
+
}
|
|
72
|
+
export function parseTaskList(output) {
|
|
73
|
+
const tasks = [];
|
|
74
|
+
const regex = /^(\S+) - /gm;
|
|
75
|
+
let match;
|
|
76
|
+
while ((match = regex.exec(output)) !== null) {
|
|
77
|
+
tasks.push(match[1]);
|
|
78
|
+
}
|
|
79
|
+
return tasks;
|
|
80
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Bounds {
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
right: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
}
|
|
7
|
+
export interface AccessibilityNode {
|
|
8
|
+
index: number;
|
|
9
|
+
text: string;
|
|
10
|
+
resourceId: string;
|
|
11
|
+
className: string;
|
|
12
|
+
contentDesc: string;
|
|
13
|
+
bounds: Bounds;
|
|
14
|
+
centerX: number;
|
|
15
|
+
centerY: number;
|
|
16
|
+
clickable: boolean;
|
|
17
|
+
focusable: boolean;
|
|
18
|
+
children?: AccessibilityNode[];
|
|
19
|
+
}
|
|
20
|
+
export declare function parseUiDump(xml: string): AccessibilityNode[];
|
|
21
|
+
export declare function flattenTree(nodes: AccessibilityNode[]): AccessibilityNode[];
|
|
22
|
+
export declare function findElements(nodes: AccessibilityNode[], selector: {
|
|
23
|
+
resourceId?: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
textContains?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
}): AccessibilityNode[];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
function parseAttributes(attrStr) {
|
|
2
|
+
const attrs = {};
|
|
3
|
+
const attrRegex = /(\w+(?:-\w+)?)="([^"]*)"/g;
|
|
4
|
+
let match;
|
|
5
|
+
while ((match = attrRegex.exec(attrStr)) !== null) {
|
|
6
|
+
attrs[match[1]] = match[2];
|
|
7
|
+
}
|
|
8
|
+
return attrs;
|
|
9
|
+
}
|
|
10
|
+
function parseBounds(boundsStr) {
|
|
11
|
+
const match = boundsStr.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
12
|
+
if (!match)
|
|
13
|
+
return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
14
|
+
return {
|
|
15
|
+
left: parseInt(match[1], 10),
|
|
16
|
+
top: parseInt(match[2], 10),
|
|
17
|
+
right: parseInt(match[3], 10),
|
|
18
|
+
bottom: parseInt(match[4], 10),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function parseNodeFromAttrs(attrs) {
|
|
22
|
+
const bounds = parseBounds(attrs.bounds || "[0,0][0,0]");
|
|
23
|
+
return {
|
|
24
|
+
index: parseInt(attrs.index || "0", 10),
|
|
25
|
+
text: attrs.text || "",
|
|
26
|
+
resourceId: attrs["resource-id"] || "",
|
|
27
|
+
className: attrs.class || "",
|
|
28
|
+
contentDesc: attrs["content-desc"] || "",
|
|
29
|
+
bounds,
|
|
30
|
+
centerX: Math.round((bounds.left + bounds.right) / 2),
|
|
31
|
+
centerY: Math.round((bounds.top + bounds.bottom) / 2),
|
|
32
|
+
clickable: attrs.clickable === "true",
|
|
33
|
+
focusable: attrs.focusable === "true",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function parseUiDump(xml) {
|
|
37
|
+
const nodes = [];
|
|
38
|
+
// Find all node elements and build tree structure
|
|
39
|
+
// Using a simple recursive parser
|
|
40
|
+
function parseChildren(content) {
|
|
41
|
+
const children = [];
|
|
42
|
+
// Match top-level node elements only
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let nodeStart = -1;
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < content.length) {
|
|
47
|
+
// Check for node opening tag
|
|
48
|
+
if (content.slice(i, i + 5) === "<node") {
|
|
49
|
+
if (depth === 0) {
|
|
50
|
+
nodeStart = i;
|
|
51
|
+
}
|
|
52
|
+
depth++;
|
|
53
|
+
// Skip to end of opening tag
|
|
54
|
+
const tagEnd = content.indexOf(">", i);
|
|
55
|
+
if (tagEnd === -1)
|
|
56
|
+
break;
|
|
57
|
+
// Check if self-closing
|
|
58
|
+
if (content[tagEnd - 1] === "/") {
|
|
59
|
+
depth--;
|
|
60
|
+
if (depth === 0 && nodeStart !== -1) {
|
|
61
|
+
const nodeXml = content.slice(nodeStart, tagEnd + 1);
|
|
62
|
+
const attrMatch = nodeXml.match(/<node\s+([^>]*)/);
|
|
63
|
+
if (attrMatch) {
|
|
64
|
+
const node = parseNodeFromAttrs(parseAttributes(attrMatch[1]));
|
|
65
|
+
children.push(node);
|
|
66
|
+
}
|
|
67
|
+
nodeStart = -1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
i = tagEnd + 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Check for closing tag
|
|
74
|
+
if (content.slice(i, i + 7) === "</node>") {
|
|
75
|
+
depth--;
|
|
76
|
+
if (depth === 0 && nodeStart !== -1) {
|
|
77
|
+
const nodeXml = content.slice(nodeStart, i + 7);
|
|
78
|
+
// Parse this node's attributes
|
|
79
|
+
const attrMatch = nodeXml.match(/<node\s+([^>]*)>/);
|
|
80
|
+
if (attrMatch) {
|
|
81
|
+
const node = parseNodeFromAttrs(parseAttributes(attrMatch[1]));
|
|
82
|
+
// Parse children recursively
|
|
83
|
+
const innerStart = nodeXml.indexOf(">") + 1;
|
|
84
|
+
const innerEnd = nodeXml.lastIndexOf("</node>");
|
|
85
|
+
if (innerEnd > innerStart) {
|
|
86
|
+
const innerContent = nodeXml.slice(innerStart, innerEnd);
|
|
87
|
+
const childNodes = parseChildren(innerContent);
|
|
88
|
+
if (childNodes.length > 0) {
|
|
89
|
+
node.children = childNodes;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
children.push(node);
|
|
93
|
+
}
|
|
94
|
+
nodeStart = -1;
|
|
95
|
+
}
|
|
96
|
+
i += 7;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
return children;
|
|
102
|
+
}
|
|
103
|
+
// Extract hierarchy content
|
|
104
|
+
const hierarchyMatch = xml.match(/<hierarchy[^>]*>([\s\S]*)<\/hierarchy>/);
|
|
105
|
+
if (hierarchyMatch) {
|
|
106
|
+
return parseChildren(hierarchyMatch[1]);
|
|
107
|
+
}
|
|
108
|
+
return nodes;
|
|
109
|
+
}
|
|
110
|
+
export function flattenTree(nodes) {
|
|
111
|
+
const flat = [];
|
|
112
|
+
function walk(node) {
|
|
113
|
+
flat.push(node);
|
|
114
|
+
if (node.children) {
|
|
115
|
+
for (const child of node.children) {
|
|
116
|
+
walk(child);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const node of nodes) {
|
|
121
|
+
walk(node);
|
|
122
|
+
}
|
|
123
|
+
return flat;
|
|
124
|
+
}
|
|
125
|
+
export function findElements(nodes, selector) {
|
|
126
|
+
const flat = flattenTree(nodes);
|
|
127
|
+
return flat.filter((node) => {
|
|
128
|
+
if (selector.resourceId && !node.resourceId.includes(selector.resourceId)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (selector.text && node.text !== selector.text) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (selector.textContains && !node.text.includes(selector.textContains)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (selector.className && !node.className.includes(selector.className)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { CacheManager, DeviceStateManager, ProcessRunner } from "./services/index.js";
|
|
3
|
+
import { AdbAdapter, EmulatorAdapter, GradleAdapter, UiAutomatorAdapter } from "./adapters/index.js";
|
|
4
|
+
export interface ServerContext {
|
|
5
|
+
cache: CacheManager;
|
|
6
|
+
deviceState: DeviceStateManager;
|
|
7
|
+
processRunner: ProcessRunner;
|
|
8
|
+
adb: AdbAdapter;
|
|
9
|
+
emulator: EmulatorAdapter;
|
|
10
|
+
gradle: GradleAdapter;
|
|
11
|
+
ui: UiAutomatorAdapter;
|
|
12
|
+
}
|
|
13
|
+
export declare function createServerContext(): ServerContext;
|
|
14
|
+
export declare function createServer(context: ServerContext): Promise<Server>;
|
|
15
|
+
export declare function runServer(): Promise<void>;
|