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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ProcessRunner } from "../services/index.js";
|
|
2
|
+
import { ReplicantError, ErrorCode } from "../types/index.js";
|
|
3
|
+
import { parseBuildOutput, parseTestOutput, parseModuleList, parseVariantList, parseTaskList, } from "../parsers/gradle-output.js";
|
|
4
|
+
export class GradleAdapter {
|
|
5
|
+
runner;
|
|
6
|
+
projectPath;
|
|
7
|
+
constructor(runner = new ProcessRunner(), projectPath) {
|
|
8
|
+
this.runner = runner;
|
|
9
|
+
this.projectPath = projectPath;
|
|
10
|
+
}
|
|
11
|
+
async build(operation, module, flavor) {
|
|
12
|
+
const task = module ? `${module}:${operation}` : operation;
|
|
13
|
+
const args = [task];
|
|
14
|
+
if (flavor) {
|
|
15
|
+
args.push(`-Pflavor=${flavor}`);
|
|
16
|
+
}
|
|
17
|
+
const result = await this.gradle(args, 300000); // 5 min timeout for builds
|
|
18
|
+
const parsed = parseBuildOutput(result.stdout + result.stderr);
|
|
19
|
+
if (!parsed.success) {
|
|
20
|
+
throw new ReplicantError(ErrorCode.BUILD_FAILED, `Build failed: ${parsed.failedTask || "unknown error"}`, "Check gradle-get-details for full error output", { buildResult: parsed });
|
|
21
|
+
}
|
|
22
|
+
return { result: parsed, fullOutput: result.stdout + result.stderr };
|
|
23
|
+
}
|
|
24
|
+
async test(operation, module, filter) {
|
|
25
|
+
const taskName = operation === "unitTest" ? "testDebugUnitTest" : "connectedDebugAndroidTest";
|
|
26
|
+
const task = module ? `${module}:${taskName}` : taskName;
|
|
27
|
+
const args = [task];
|
|
28
|
+
if (filter) {
|
|
29
|
+
args.push("--tests", filter);
|
|
30
|
+
}
|
|
31
|
+
const result = await this.gradle(args, 600000); // 10 min timeout for tests
|
|
32
|
+
const parsed = parseTestOutput(result.stdout + result.stderr);
|
|
33
|
+
return { result: parsed, fullOutput: result.stdout + result.stderr };
|
|
34
|
+
}
|
|
35
|
+
async listModules() {
|
|
36
|
+
const result = await this.gradle(["projects"]);
|
|
37
|
+
return parseModuleList(result.stdout);
|
|
38
|
+
}
|
|
39
|
+
async listVariants(module) {
|
|
40
|
+
// Try to get variants - this varies by project setup
|
|
41
|
+
const task = module ? `${module}:printVariants` : "printVariants";
|
|
42
|
+
try {
|
|
43
|
+
const result = await this.gradle([task]);
|
|
44
|
+
return parseVariantList(result.stdout);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Fallback: return common defaults
|
|
48
|
+
return [
|
|
49
|
+
{ name: "debug", buildType: "debug", flavors: [] },
|
|
50
|
+
{ name: "release", buildType: "release", flavors: [] },
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async listTasks(module) {
|
|
55
|
+
const args = module ? [`${module}:tasks`, "--all"] : ["tasks", "--all"];
|
|
56
|
+
const result = await this.gradle(args);
|
|
57
|
+
return parseTaskList(result.stdout);
|
|
58
|
+
}
|
|
59
|
+
async clean(stopDaemons = false) {
|
|
60
|
+
await this.gradle(["clean"]);
|
|
61
|
+
if (stopDaemons) {
|
|
62
|
+
await this.gradle(["--stop"]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async gradle(args, timeoutMs = 120000) {
|
|
66
|
+
const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
67
|
+
try {
|
|
68
|
+
return await this.runner.run(gradleCmd, args, {
|
|
69
|
+
timeoutMs,
|
|
70
|
+
cwd: this.projectPath,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
75
|
+
throw new ReplicantError(ErrorCode.GRADLE_NOT_FOUND, "Gradle wrapper not found", "Ensure you are in an Android project directory with gradlew");
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AdbAdapter } from "./adb.js";
|
|
2
|
+
import { AccessibilityNode } from "../parsers/ui-dump.js";
|
|
3
|
+
export declare class UiAutomatorAdapter {
|
|
4
|
+
private adb;
|
|
5
|
+
constructor(adb?: AdbAdapter);
|
|
6
|
+
dump(deviceId: string): Promise<AccessibilityNode[]>;
|
|
7
|
+
find(deviceId: string, selector: {
|
|
8
|
+
resourceId?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
textContains?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): Promise<AccessibilityNode[]>;
|
|
13
|
+
tap(deviceId: string, x: number, y: number): Promise<void>;
|
|
14
|
+
tapElement(deviceId: string, element: AccessibilityNode): Promise<void>;
|
|
15
|
+
input(deviceId: string, text: string): Promise<void>;
|
|
16
|
+
screenshot(deviceId: string, localPath: string): Promise<void>;
|
|
17
|
+
accessibilityCheck(deviceId: string): Promise<{
|
|
18
|
+
hasAccessibleElements: boolean;
|
|
19
|
+
clickableCount: number;
|
|
20
|
+
textCount: number;
|
|
21
|
+
totalElements: number;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AdbAdapter } from "./adb.js";
|
|
2
|
+
import { parseUiDump, findElements, flattenTree } from "../parsers/ui-dump.js";
|
|
3
|
+
export class UiAutomatorAdapter {
|
|
4
|
+
adb;
|
|
5
|
+
constructor(adb = new AdbAdapter()) {
|
|
6
|
+
this.adb = adb;
|
|
7
|
+
}
|
|
8
|
+
async dump(deviceId) {
|
|
9
|
+
// Dump UI hierarchy to device
|
|
10
|
+
await this.adb.shell(deviceId, "uiautomator dump /sdcard/ui-dump.xml");
|
|
11
|
+
// Pull the dump
|
|
12
|
+
const result = await this.adb.shell(deviceId, "cat /sdcard/ui-dump.xml");
|
|
13
|
+
// Clean up
|
|
14
|
+
await this.adb.shell(deviceId, "rm /sdcard/ui-dump.xml");
|
|
15
|
+
return parseUiDump(result.stdout);
|
|
16
|
+
}
|
|
17
|
+
async find(deviceId, selector) {
|
|
18
|
+
const tree = await this.dump(deviceId);
|
|
19
|
+
return findElements(tree, selector);
|
|
20
|
+
}
|
|
21
|
+
async tap(deviceId, x, y) {
|
|
22
|
+
await this.adb.shell(deviceId, `input tap ${x} ${y}`);
|
|
23
|
+
}
|
|
24
|
+
async tapElement(deviceId, element) {
|
|
25
|
+
await this.tap(deviceId, element.centerX, element.centerY);
|
|
26
|
+
}
|
|
27
|
+
async input(deviceId, text) {
|
|
28
|
+
// Escape special characters for shell
|
|
29
|
+
const escaped = text.replace(/(['"\\$`])/g, "\\$1").replace(/ /g, "%s");
|
|
30
|
+
await this.adb.shell(deviceId, `input text "${escaped}"`);
|
|
31
|
+
}
|
|
32
|
+
async screenshot(deviceId, localPath) {
|
|
33
|
+
const remotePath = "/sdcard/screenshot.png";
|
|
34
|
+
await this.adb.shell(deviceId, `screencap -p ${remotePath}`);
|
|
35
|
+
// Pull to local (using adb pull via shell workaround)
|
|
36
|
+
// In real implementation, would use adb pull directly
|
|
37
|
+
const result = await this.adb.shell(deviceId, `base64 ${remotePath}`);
|
|
38
|
+
// For now, just verify it worked
|
|
39
|
+
await this.adb.shell(deviceId, `rm ${remotePath}`);
|
|
40
|
+
}
|
|
41
|
+
async accessibilityCheck(deviceId) {
|
|
42
|
+
const tree = await this.dump(deviceId);
|
|
43
|
+
const flat = flattenTree(tree);
|
|
44
|
+
const clickableCount = flat.filter((n) => n.clickable).length;
|
|
45
|
+
const textCount = flat.filter((n) => n.text || n.contentDesc).length;
|
|
46
|
+
return {
|
|
47
|
+
hasAccessibleElements: textCount > 0,
|
|
48
|
+
clickableCount,
|
|
49
|
+
textCount,
|
|
50
|
+
totalElements: flat.length,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
package/dist/cli/adb.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { AdbAdapter } from "../adapters/index.js";
|
|
3
|
+
import { DeviceStateManager, CacheManager } from "../services/index.js";
|
|
4
|
+
import { formatDeviceList, formatLogcat } from "./formatter.js";
|
|
5
|
+
import { CACHE_TTLS } from "../types/index.js";
|
|
6
|
+
const adapter = new AdbAdapter();
|
|
7
|
+
const deviceState = new DeviceStateManager();
|
|
8
|
+
const cache = new CacheManager();
|
|
9
|
+
export function createAdbCommand() {
|
|
10
|
+
const adb = new Command("adb").description("ADB device and app management");
|
|
11
|
+
// Devices subcommand
|
|
12
|
+
adb
|
|
13
|
+
.command("devices")
|
|
14
|
+
.description("List connected devices")
|
|
15
|
+
.option("--json", "Output as JSON")
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
const devices = await adapter.getDevices();
|
|
19
|
+
const currentDevice = deviceState.getCurrentDevice();
|
|
20
|
+
// Auto-select if single device
|
|
21
|
+
const autoSelected = deviceState.autoSelectIfSingle(devices);
|
|
22
|
+
const deviceInfos = devices.map((d) => ({
|
|
23
|
+
id: d.id,
|
|
24
|
+
name: d.name,
|
|
25
|
+
state: d.status,
|
|
26
|
+
selected: currentDevice?.id === d.id || (autoSelected && devices[0].id === d.id),
|
|
27
|
+
}));
|
|
28
|
+
if (options.json) {
|
|
29
|
+
console.log(JSON.stringify({ devices: deviceInfos }, null, 2));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(formatDeviceList({ devices: deviceInfos }));
|
|
33
|
+
if (autoSelected) {
|
|
34
|
+
console.log(`\nAuto-selected: ${devices[0].id}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
40
|
+
console.error(`Error: ${errorMessage}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// Select subcommand
|
|
45
|
+
adb
|
|
46
|
+
.command("select <deviceId>")
|
|
47
|
+
.description("Set active device for subsequent commands")
|
|
48
|
+
.action(async (deviceId) => {
|
|
49
|
+
try {
|
|
50
|
+
const devices = await adapter.getDevices();
|
|
51
|
+
const device = devices.find((d) => d.id === deviceId);
|
|
52
|
+
if (!device) {
|
|
53
|
+
console.error(`Device not found: ${deviceId}`);
|
|
54
|
+
console.error("Available devices:");
|
|
55
|
+
devices.forEach((d) => console.error(` ${d.id} (${d.name})`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
deviceState.setCurrentDevice(device);
|
|
59
|
+
console.log(`Selected device: ${device.id} (${device.name})`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
63
|
+
console.error(`Error: ${errorMessage}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Install subcommand
|
|
68
|
+
adb
|
|
69
|
+
.command("install <apkPath>")
|
|
70
|
+
.description("Install APK on the active device")
|
|
71
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
72
|
+
.action(async (apkPath, options) => {
|
|
73
|
+
try {
|
|
74
|
+
const deviceId = options.device || getDeviceId();
|
|
75
|
+
await adapter.install(deviceId, apkPath);
|
|
76
|
+
console.log(`Installed: ${apkPath}`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
80
|
+
console.error(`Error: ${errorMessage}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// Launch subcommand
|
|
85
|
+
adb
|
|
86
|
+
.command("launch <package>")
|
|
87
|
+
.description("Launch an app on the active device")
|
|
88
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
89
|
+
.action(async (packageName, options) => {
|
|
90
|
+
try {
|
|
91
|
+
const deviceId = options.device || getDeviceId();
|
|
92
|
+
await adapter.launch(deviceId, packageName);
|
|
93
|
+
console.log(`Launched: ${packageName}`);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
97
|
+
console.error(`Error: ${errorMessage}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// Stop subcommand
|
|
102
|
+
adb
|
|
103
|
+
.command("stop <package>")
|
|
104
|
+
.description("Force stop an app on the active device")
|
|
105
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
106
|
+
.action(async (packageName, options) => {
|
|
107
|
+
try {
|
|
108
|
+
const deviceId = options.device || getDeviceId();
|
|
109
|
+
await adapter.stop(deviceId, packageName);
|
|
110
|
+
console.log(`Stopped: ${packageName}`);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
114
|
+
console.error(`Error: ${errorMessage}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// Uninstall subcommand
|
|
119
|
+
adb
|
|
120
|
+
.command("uninstall <package>")
|
|
121
|
+
.description("Uninstall an app from the active device")
|
|
122
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
123
|
+
.action(async (packageName, options) => {
|
|
124
|
+
try {
|
|
125
|
+
const deviceId = options.device || getDeviceId();
|
|
126
|
+
await adapter.uninstall(deviceId, packageName);
|
|
127
|
+
console.log(`Uninstalled: ${packageName}`);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
131
|
+
console.error(`Error: ${errorMessage}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// Clear subcommand
|
|
136
|
+
adb
|
|
137
|
+
.command("clear <package>")
|
|
138
|
+
.description("Clear app data on the active device")
|
|
139
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
140
|
+
.action(async (packageName, options) => {
|
|
141
|
+
try {
|
|
142
|
+
const deviceId = options.device || getDeviceId();
|
|
143
|
+
await adapter.clearData(deviceId, packageName);
|
|
144
|
+
console.log(`Cleared data: ${packageName}`);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
148
|
+
console.error(`Error: ${errorMessage}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// Logcat subcommand
|
|
153
|
+
adb
|
|
154
|
+
.command("logcat")
|
|
155
|
+
.description("Read device logs")
|
|
156
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
157
|
+
.option("-l, --level <level>", "Log level filter (verbose, debug, info, warn, error)", "info")
|
|
158
|
+
.option("-n, --lines <count>", "Number of lines to retrieve", "50")
|
|
159
|
+
.option("-t, --tag <tag>", "Filter by tag")
|
|
160
|
+
.option("--json", "Output as JSON")
|
|
161
|
+
.action(async (options) => {
|
|
162
|
+
try {
|
|
163
|
+
const deviceId = options.device || getDeviceId();
|
|
164
|
+
// Build filter string
|
|
165
|
+
let filter = "";
|
|
166
|
+
if (options.tag) {
|
|
167
|
+
const levelChar = getLevelChar(options.level);
|
|
168
|
+
filter = `${options.tag}:${levelChar} *:S`;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const levelChar = getLevelChar(options.level);
|
|
172
|
+
filter = `*:${levelChar}`;
|
|
173
|
+
}
|
|
174
|
+
const logs = await adapter.logcat(deviceId, {
|
|
175
|
+
lines: parseInt(options.lines, 10),
|
|
176
|
+
filter,
|
|
177
|
+
});
|
|
178
|
+
const lines = logs.split("\n").filter((line) => line.trim());
|
|
179
|
+
const cacheId = cache.generateId("logcat");
|
|
180
|
+
cache.set(cacheId, { logs, level: options.level }, "logcat", CACHE_TTLS.LOGCAT);
|
|
181
|
+
if (options.json) {
|
|
182
|
+
console.log(JSON.stringify({
|
|
183
|
+
level: options.level,
|
|
184
|
+
count: lines.length,
|
|
185
|
+
lines,
|
|
186
|
+
cacheId,
|
|
187
|
+
}, null, 2));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
console.log(formatLogcat({
|
|
191
|
+
level: options.level,
|
|
192
|
+
count: lines.length,
|
|
193
|
+
lines,
|
|
194
|
+
cacheId,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
200
|
+
console.error(`Error: ${errorMessage}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// Shell subcommand
|
|
205
|
+
adb
|
|
206
|
+
.command("shell <command...>")
|
|
207
|
+
.description("Run a shell command on the active device")
|
|
208
|
+
.option("-d, --device <deviceId>", "Target device (uses active device if not specified)")
|
|
209
|
+
.option("--json", "Output as JSON")
|
|
210
|
+
.action(async (commandParts, options) => {
|
|
211
|
+
try {
|
|
212
|
+
const deviceId = options.device || getDeviceId();
|
|
213
|
+
const command = commandParts.join(" ");
|
|
214
|
+
const result = await adapter.shell(deviceId, command);
|
|
215
|
+
if (options.json) {
|
|
216
|
+
console.log(JSON.stringify({
|
|
217
|
+
command,
|
|
218
|
+
exitCode: result.exitCode,
|
|
219
|
+
stdout: result.stdout,
|
|
220
|
+
stderr: result.stderr,
|
|
221
|
+
}, null, 2));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (result.stdout) {
|
|
225
|
+
console.log(result.stdout);
|
|
226
|
+
}
|
|
227
|
+
if (result.stderr) {
|
|
228
|
+
console.error(result.stderr);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (result.exitCode !== 0) {
|
|
232
|
+
process.exit(result.exitCode);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
237
|
+
console.error(`Error: ${errorMessage}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return adb;
|
|
242
|
+
}
|
|
243
|
+
function getDeviceId() {
|
|
244
|
+
const device = deviceState.requireCurrentDevice();
|
|
245
|
+
return device.id;
|
|
246
|
+
}
|
|
247
|
+
function getLevelChar(level) {
|
|
248
|
+
const levels = {
|
|
249
|
+
verbose: "V",
|
|
250
|
+
debug: "D",
|
|
251
|
+
info: "I",
|
|
252
|
+
warn: "W",
|
|
253
|
+
error: "E",
|
|
254
|
+
};
|
|
255
|
+
return levels[level.toLowerCase()] || "I";
|
|
256
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { CacheManager } from "../services/index.js";
|
|
3
|
+
const cache = new CacheManager();
|
|
4
|
+
export function createCacheCommand() {
|
|
5
|
+
const cacheCmd = new Command("cache").description("Manage output cache");
|
|
6
|
+
// Stats subcommand
|
|
7
|
+
cacheCmd
|
|
8
|
+
.command("stats")
|
|
9
|
+
.description("Show cache statistics")
|
|
10
|
+
.option("--json", "Output as JSON")
|
|
11
|
+
.action((options) => {
|
|
12
|
+
try {
|
|
13
|
+
const stats = cache.getStats();
|
|
14
|
+
if (options.json) {
|
|
15
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log("Cache Statistics");
|
|
19
|
+
console.log("================");
|
|
20
|
+
console.log(`Entries: ${stats.entryCount}`);
|
|
21
|
+
console.log(`Total size: ${formatBytes(stats.totalSizeBytes)}`);
|
|
22
|
+
console.log(`Max entries: ${stats.config.maxEntries}`);
|
|
23
|
+
console.log(`Default TTL: ${formatMs(stats.config.defaultTtlMs)}`);
|
|
24
|
+
if (Object.keys(stats.typeBreakdown).length > 0) {
|
|
25
|
+
console.log("\nBy type:");
|
|
26
|
+
for (const [type, count] of Object.entries(stats.typeBreakdown)) {
|
|
27
|
+
console.log(` ${type}: ${count}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
34
|
+
console.error(`Error: ${errorMessage}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// Get subcommand
|
|
39
|
+
cacheCmd
|
|
40
|
+
.command("get <cacheId>")
|
|
41
|
+
.description("Get cached entry by ID")
|
|
42
|
+
.option("--json", "Output as JSON")
|
|
43
|
+
.action((cacheId, options) => {
|
|
44
|
+
try {
|
|
45
|
+
const entry = cache.get(cacheId);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
console.error(`Cache entry not found: ${cacheId}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (options.json) {
|
|
51
|
+
console.log(JSON.stringify({
|
|
52
|
+
id: cacheId,
|
|
53
|
+
data: entry.data,
|
|
54
|
+
metadata: entry.metadata,
|
|
55
|
+
expiresAt: entry.expiresAt,
|
|
56
|
+
}, null, 2));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(`Cache ID: ${cacheId}`);
|
|
60
|
+
console.log(`Type: ${entry.metadata.type}`);
|
|
61
|
+
console.log(`Created: ${new Date(entry.metadata.createdAt).toISOString()}`);
|
|
62
|
+
console.log(`Expires: ${new Date(entry.expiresAt).toISOString()}`);
|
|
63
|
+
console.log(`Size: ${formatBytes(entry.metadata.sizeBytes ?? 0)}`);
|
|
64
|
+
console.log("\nData:");
|
|
65
|
+
console.log(JSON.stringify(entry.data, null, 2));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
+
console.error(`Error: ${errorMessage}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Clear subcommand
|
|
75
|
+
cacheCmd
|
|
76
|
+
.command("clear")
|
|
77
|
+
.description("Clear all cached data")
|
|
78
|
+
.option("--json", "Output as JSON")
|
|
79
|
+
.action((options) => {
|
|
80
|
+
try {
|
|
81
|
+
const statsBefore = cache.getStats();
|
|
82
|
+
const entriesCleared = statsBefore.entryCount;
|
|
83
|
+
cache.clearAll();
|
|
84
|
+
if (options.json) {
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
cleared: true,
|
|
87
|
+
entriesCleared,
|
|
88
|
+
}, null, 2));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(`Cleared ${entriesCleared} cache entries`);
|
|
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
|
+
return cacheCmd;
|
|
101
|
+
}
|
|
102
|
+
function formatBytes(bytes) {
|
|
103
|
+
if (bytes === 0)
|
|
104
|
+
return "0 B";
|
|
105
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
106
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
107
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
108
|
+
}
|
|
109
|
+
function formatMs(ms) {
|
|
110
|
+
if (ms < 1000)
|
|
111
|
+
return `${ms}ms`;
|
|
112
|
+
if (ms < 60000)
|
|
113
|
+
return `${(ms / 1000).toFixed(0)}s`;
|
|
114
|
+
return `${(ms / 60000).toFixed(0)}m`;
|
|
115
|
+
}
|