@sensaiorg/adapter-android 0.1.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 (89) hide show
  1. package/dist/android-adapter.d.ts.map +1 -0
  2. package/dist/android-adapter.js +89 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +12 -0
  5. package/dist/tools/accessibility.d.ts.map +1 -0
  6. package/dist/tools/accessibility.js +85 -0
  7. package/dist/tools/adb.d.ts.map +1 -0
  8. package/dist/tools/adb.js +66 -0
  9. package/dist/tools/app-state.d.ts.map +1 -0
  10. package/dist/tools/app-state.js +173 -0
  11. package/dist/tools/diagnose.d.ts.map +1 -0
  12. package/dist/tools/diagnose.js +128 -0
  13. package/dist/tools/hot-reload.d.ts.map +1 -0
  14. package/dist/tools/hot-reload.js +97 -0
  15. package/dist/tools/index.d.ts.map +1 -0
  16. package/dist/tools/index.js +66 -0
  17. package/dist/tools/interaction.d.ts.map +1 -0
  18. package/dist/tools/interaction.js +395 -0
  19. package/dist/tools/logcat.d.ts.map +1 -0
  20. package/dist/tools/logcat.js +216 -0
  21. package/dist/tools/network.d.ts.map +1 -0
  22. package/dist/tools/network.js +123 -0
  23. package/dist/tools/performance.d.ts.map +1 -0
  24. package/dist/tools/performance.js +143 -0
  25. package/dist/tools/recording.d.ts.map +1 -0
  26. package/dist/tools/recording.js +102 -0
  27. package/dist/tools/rn-tools.d.ts.map +1 -0
  28. package/dist/tools/rn-tools.js +120 -0
  29. package/dist/tools/smart-actions.d.ts.map +1 -0
  30. package/dist/tools/smart-actions.js +506 -0
  31. package/dist/tools/ui-tree.d.ts.map +1 -0
  32. package/dist/tools/ui-tree.js +226 -0
  33. package/dist/transport/adb-client.d.ts.map +1 -0
  34. package/dist/transport/adb-client.js +124 -0
  35. package/dist/transport/adb-client.test.d.ts.map +1 -0
  36. package/dist/transport/adb-client.test.js +153 -0
  37. package/dist/transport/agent-client.d.ts.map +1 -0
  38. package/dist/transport/agent-client.js +157 -0
  39. package/dist/transport/agent-client.test.d.ts.map +1 -0
  40. package/dist/transport/agent-client.test.js +199 -0
  41. package/dist/transport/connection-manager.d.ts.map +1 -0
  42. package/dist/transport/connection-manager.js +119 -0
  43. package/dist/util/logcat-parser.d.ts.map +1 -0
  44. package/dist/util/logcat-parser.js +79 -0
  45. package/dist/util/safety.d.ts.map +1 -0
  46. package/dist/util/safety.js +132 -0
  47. package/dist/util/safety.test.d.ts.map +1 -0
  48. package/dist/util/safety.test.js +205 -0
  49. package/dist/util/text-extractor.d.ts.map +1 -0
  50. package/dist/util/text-extractor.js +71 -0
  51. package/dist/util/ui-tree-cache.d.ts.map +1 -0
  52. package/dist/util/ui-tree-cache.js +46 -0
  53. package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
  54. package/dist/util/ui-tree-cache.test.js +84 -0
  55. package/dist/util/ui-tree-parser.d.ts.map +1 -0
  56. package/dist/util/ui-tree-parser.js +123 -0
  57. package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
  58. package/dist/util/ui-tree-parser.test.js +167 -0
  59. package/package.json +22 -0
  60. package/src/android-adapter.ts +124 -0
  61. package/src/index.ts +8 -0
  62. package/src/tools/accessibility.ts +94 -0
  63. package/src/tools/adb.ts +75 -0
  64. package/src/tools/app-state.ts +193 -0
  65. package/src/tools/diagnose.ts +146 -0
  66. package/src/tools/hot-reload.ts +103 -0
  67. package/src/tools/index.ts +66 -0
  68. package/src/tools/interaction.ts +448 -0
  69. package/src/tools/logcat.ts +252 -0
  70. package/src/tools/network.ts +145 -0
  71. package/src/tools/performance.ts +169 -0
  72. package/src/tools/recording.ts +123 -0
  73. package/src/tools/rn-tools.ts +143 -0
  74. package/src/tools/smart-actions.ts +593 -0
  75. package/src/tools/ui-tree.ts +258 -0
  76. package/src/transport/adb-client.test.ts +228 -0
  77. package/src/transport/adb-client.ts +139 -0
  78. package/src/transport/agent-client.test.ts +267 -0
  79. package/src/transport/agent-client.ts +188 -0
  80. package/src/transport/connection-manager.ts +140 -0
  81. package/src/util/logcat-parser.ts +94 -0
  82. package/src/util/safety.test.ts +251 -0
  83. package/src/util/safety.ts +143 -0
  84. package/src/util/text-extractor.ts +87 -0
  85. package/src/util/ui-tree-cache.test.ts +105 -0
  86. package/src/util/ui-tree-cache.ts +54 -0
  87. package/src/util/ui-tree-parser.test.ts +182 -0
  88. package/src/util/ui-tree-parser.ts +169 -0
  89. package/tsconfig.json +11 -0
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const node_events_1 = require("node:events");
5
+ const agent_client_js_1 = require("./agent-client.js");
6
+ // --- Mock Socket ---
7
+ class MockSocket extends node_events_1.EventEmitter {
8
+ write = vitest_1.vi.fn((_data, _enc, cb) => {
9
+ cb?.();
10
+ return true;
11
+ });
12
+ destroy = vitest_1.vi.fn();
13
+ }
14
+ let mockSocket;
15
+ vitest_1.vi.mock("node:net", () => ({
16
+ createConnection: () => {
17
+ mockSocket = new MockSocket();
18
+ // Schedule the "connect" event by default (tests can override before it fires)
19
+ return mockSocket;
20
+ },
21
+ }));
22
+ (0, vitest_1.describe)("AgentClient", () => {
23
+ let client;
24
+ (0, vitest_1.beforeEach)(() => {
25
+ vitest_1.vi.useFakeTimers();
26
+ client = new agent_client_js_1.AgentClient("127.0.0.1", 9222);
27
+ });
28
+ (0, vitest_1.afterEach)(() => {
29
+ vitest_1.vi.useRealTimers();
30
+ vitest_1.vi.clearAllMocks();
31
+ });
32
+ // Helper: connect the client by emitting the connect event
33
+ async function connectClient() {
34
+ const p = client.connect();
35
+ mockSocket.emit("connect");
36
+ await p;
37
+ }
38
+ (0, vitest_1.describe)("connect()", () => {
39
+ (0, vitest_1.it)("resolves on successful connection", async () => {
40
+ const p = client.connect();
41
+ // Socket emits connect
42
+ mockSocket.emit("connect");
43
+ await (0, vitest_1.expect)(p).resolves.toBeUndefined();
44
+ (0, vitest_1.expect)(client.isConnected()).toBe(true);
45
+ });
46
+ (0, vitest_1.it)("rejects on timeout", async () => {
47
+ const p = client.connect();
48
+ // Advance past the 5s connect timeout
49
+ vitest_1.vi.advanceTimersByTime(5_001);
50
+ await (0, vitest_1.expect)(p).rejects.toThrow("Agent connection timed out");
51
+ (0, vitest_1.expect)(mockSocket.destroy).toHaveBeenCalled();
52
+ });
53
+ (0, vitest_1.it)("rejects on connection error", async () => {
54
+ const p = client.connect();
55
+ mockSocket.emit("error", new Error("ECONNREFUSED"));
56
+ await (0, vitest_1.expect)(p).rejects.toThrow("Agent connection failed: ECONNREFUSED");
57
+ (0, vitest_1.expect)(client.isConnected()).toBe(false);
58
+ });
59
+ (0, vitest_1.it)("reuses existing connection (no-op)", async () => {
60
+ await connectClient();
61
+ // Second connect should resolve immediately without creating a new socket
62
+ await (0, vitest_1.expect)(client.connect()).resolves.toBeUndefined();
63
+ });
64
+ });
65
+ (0, vitest_1.describe)("call()", () => {
66
+ (0, vitest_1.it)("sends JSON-RPC request with incremented id", async () => {
67
+ await connectClient();
68
+ // Fire first call
69
+ const p1 = client.call("getTree", { depth: 2 });
70
+ const payload1 = JSON.parse(mockSocket.write.mock.calls[0][0].trim());
71
+ (0, vitest_1.expect)(payload1).toEqual({
72
+ jsonrpc: "2.0",
73
+ id: 1,
74
+ method: "getTree",
75
+ params: { depth: 2 },
76
+ });
77
+ // Respond
78
+ mockSocket.emit("data", Buffer.from(JSON.stringify({ id: 1, result: "ok" }) + "\n"));
79
+ await p1;
80
+ // Fire second call
81
+ const p2 = client.call("getState");
82
+ const payload2 = JSON.parse(mockSocket.write.mock.calls[1][0].trim());
83
+ (0, vitest_1.expect)(payload2.id).toBe(2);
84
+ mockSocket.emit("data", Buffer.from(JSON.stringify({ id: 2, result: {} }) + "\n"));
85
+ await p2;
86
+ });
87
+ (0, vitest_1.it)("resolves with result from response", async () => {
88
+ await connectClient();
89
+ const p = client.call("getVersion");
90
+ mockSocket.emit("data", Buffer.from(JSON.stringify({ id: 1, result: { version: "1.0" } }) + "\n"));
91
+ await (0, vitest_1.expect)(p).resolves.toEqual({ version: "1.0" });
92
+ });
93
+ (0, vitest_1.it)("rejects on RPC error response", async () => {
94
+ await connectClient();
95
+ const p = client.call("badMethod");
96
+ mockSocket.emit("data", Buffer.from(JSON.stringify({
97
+ id: 1,
98
+ error: { code: -32601, message: "Method not found" },
99
+ }) + "\n"));
100
+ await (0, vitest_1.expect)(p).rejects.toThrow("RPC error (-32601): Method not found");
101
+ });
102
+ (0, vitest_1.it)("rejects on timeout", async () => {
103
+ await connectClient();
104
+ const p = client.call("slowMethod", undefined, 2_000);
105
+ // No response comes; advance past timeout
106
+ vitest_1.vi.advanceTimersByTime(2_001);
107
+ await (0, vitest_1.expect)(p).rejects.toThrow("RPC call 'slowMethod' timed out after 2000ms");
108
+ });
109
+ (0, vitest_1.it)("rejects if not connected", async () => {
110
+ await (0, vitest_1.expect)(client.call("anyMethod")).rejects.toThrow("Agent not connected. Call connect() first.");
111
+ });
112
+ (0, vitest_1.it)("sends empty params when none provided", async () => {
113
+ await connectClient();
114
+ const p = client.call("ping");
115
+ const payload = JSON.parse(mockSocket.write.mock.calls[0][0].trim());
116
+ (0, vitest_1.expect)(payload.params).toEqual({});
117
+ mockSocket.emit("data", Buffer.from(JSON.stringify({ id: 1, result: "pong" }) + "\n"));
118
+ await p;
119
+ });
120
+ (0, vitest_1.it)("rejects when socket write fails", async () => {
121
+ await connectClient();
122
+ mockSocket.write.mockImplementationOnce((_data, _enc, cb) => {
123
+ cb?.(new Error("broken pipe"));
124
+ return false;
125
+ });
126
+ await (0, vitest_1.expect)(client.call("test")).rejects.toThrow("Failed to send RPC: broken pipe");
127
+ });
128
+ });
129
+ (0, vitest_1.describe)("disconnect()", () => {
130
+ (0, vitest_1.it)("cleans up socket and pending requests", async () => {
131
+ await connectClient();
132
+ // Create a pending request
133
+ const p = client.call("longRunning");
134
+ client.disconnect();
135
+ (0, vitest_1.expect)(mockSocket.destroy).toHaveBeenCalled();
136
+ (0, vitest_1.expect)(client.isConnected()).toBe(false);
137
+ await (0, vitest_1.expect)(p).rejects.toThrow("Agent disconnected");
138
+ });
139
+ });
140
+ (0, vitest_1.describe)("isConnected()", () => {
141
+ (0, vitest_1.it)("returns false before connecting", () => {
142
+ (0, vitest_1.expect)(client.isConnected()).toBe(false);
143
+ });
144
+ (0, vitest_1.it)("returns true after connecting", async () => {
145
+ await connectClient();
146
+ (0, vitest_1.expect)(client.isConnected()).toBe(true);
147
+ });
148
+ (0, vitest_1.it)("returns false after disconnect", async () => {
149
+ await connectClient();
150
+ client.disconnect();
151
+ (0, vitest_1.expect)(client.isConnected()).toBe(false);
152
+ });
153
+ });
154
+ (0, vitest_1.describe)("buffer processing", () => {
155
+ (0, vitest_1.it)("handles multiple messages in one chunk", async () => {
156
+ await connectClient();
157
+ const p1 = client.call("method1");
158
+ const p2 = client.call("method2");
159
+ // Send both responses in a single data chunk
160
+ const combined = JSON.stringify({ id: 1, result: "r1" }) +
161
+ "\n" +
162
+ JSON.stringify({ id: 2, result: "r2" }) +
163
+ "\n";
164
+ mockSocket.emit("data", Buffer.from(combined));
165
+ await (0, vitest_1.expect)(p1).resolves.toBe("r1");
166
+ await (0, vitest_1.expect)(p2).resolves.toBe("r2");
167
+ });
168
+ (0, vitest_1.it)("handles split messages across chunks", async () => {
169
+ await connectClient();
170
+ const p = client.call("method1");
171
+ const full = JSON.stringify({ id: 1, result: "split-result" }) + "\n";
172
+ const mid = Math.floor(full.length / 2);
173
+ // Send first half
174
+ mockSocket.emit("data", Buffer.from(full.slice(0, mid)));
175
+ // Send second half
176
+ mockSocket.emit("data", Buffer.from(full.slice(mid)));
177
+ await (0, vitest_1.expect)(p).resolves.toBe("split-result");
178
+ });
179
+ });
180
+ (0, vitest_1.describe)("disconnect rejects all pending requests", () => {
181
+ (0, vitest_1.it)("rejects multiple pending requests on disconnect", async () => {
182
+ await connectClient();
183
+ const p1 = client.call("a");
184
+ const p2 = client.call("b");
185
+ const p3 = client.call("c");
186
+ client.disconnect();
187
+ await (0, vitest_1.expect)(p1).rejects.toThrow("Agent disconnected");
188
+ await (0, vitest_1.expect)(p2).rejects.toThrow("Agent disconnected");
189
+ await (0, vitest_1.expect)(p3).rejects.toThrow("Agent disconnected");
190
+ });
191
+ (0, vitest_1.it)("rejects pending requests on socket close event", async () => {
192
+ await connectClient();
193
+ const p = client.call("pending");
194
+ mockSocket.emit("close");
195
+ await (0, vitest_1.expect)(p).rejects.toThrow("Agent disconnected: Socket closed");
196
+ });
197
+ });
198
+ });
199
+ //# sourceMappingURL=agent-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../src/transport/connection-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAWvD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,iBAAiB,CAAK;gBAElB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAQ9D;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;IA0B7C;;OAEG;IACH,SAAS,IAAI,gBAAgB;IAS7B;;OAEG;IACH,QAAQ,IAAI,IAAI;IAUhB,iDAAiD;YACnC,eAAe;IAS7B,qEAAqE;IACrE,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,KAAK;CAGd"}
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Connection Manager - Maintains ADB and agent connections with health checks
4
+ * and automatic reconnection.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ConnectionManager = void 0;
8
+ const adb_client_js_1 = require("./adb-client.js");
9
+ const agent_client_js_1 = require("./agent-client.js");
10
+ const ui_tree_cache_js_1 = require("../util/ui-tree-cache.js");
11
+ /** How often to run health checks (ms). */
12
+ const HEALTH_CHECK_INTERVAL_MS = 30_000;
13
+ /** Delay before attempting agent reconnection (ms). */
14
+ const RECONNECT_DELAY_MS = 5_000;
15
+ /** Maximum consecutive agent reconnect failures before backing off. */
16
+ const MAX_RECONNECT_ATTEMPTS = 3;
17
+ class ConnectionManager {
18
+ adb;
19
+ agent;
20
+ uiCache;
21
+ agentPort;
22
+ device;
23
+ healthTimer = null;
24
+ reconnectAttempts = 0;
25
+ constructor(adbPath, device, agentPort) {
26
+ this.device = device;
27
+ this.agentPort = agentPort;
28
+ this.adb = new adb_client_js_1.AdbClient(adbPath, device);
29
+ this.agent = new agent_client_js_1.AgentClient("127.0.0.1", agentPort);
30
+ this.uiCache = new ui_tree_cache_js_1.UiTreeCache();
31
+ }
32
+ /**
33
+ * Initialize connections: verify ADB, set up port forwarding, attempt agent connection.
34
+ * ADB must succeed; agent connection is best-effort (Phase 2 feature).
35
+ */
36
+ async initialize() {
37
+ // ADB is required
38
+ const adbOk = await this.adb.isConnected();
39
+ if (!adbOk) {
40
+ throw new Error(`Device ${this.device} not reachable via ADB. ` +
41
+ `Ensure the emulator/device is running and 'adb devices' lists it.`);
42
+ }
43
+ // Forward the agent port (best-effort)
44
+ try {
45
+ await this.adb.forward(this.agentPort, this.agentPort);
46
+ }
47
+ catch {
48
+ // Port forwarding may fail if agent isn't installed yet - that's fine
49
+ }
50
+ // Try connecting to the on-device agent (optional for Phase 1)
51
+ await this.tryConnectAgent();
52
+ // Start periodic health checks
53
+ this.startHealthChecks();
54
+ return this.getStatus();
55
+ }
56
+ /**
57
+ * Get current connection status.
58
+ */
59
+ getStatus() {
60
+ return {
61
+ adb: this.adb.isDeviceConnected,
62
+ agent: this.agent.isConnected(),
63
+ device: this.device,
64
+ agentPort: this.agentPort,
65
+ };
66
+ }
67
+ /**
68
+ * Cleanly shut down connections and timers.
69
+ */
70
+ shutdown() {
71
+ if (this.healthTimer) {
72
+ clearInterval(this.healthTimer);
73
+ this.healthTimer = null;
74
+ }
75
+ this.agent.disconnect();
76
+ }
77
+ // --- Private helpers ---
78
+ /** Attempt agent connection without throwing. */
79
+ async tryConnectAgent() {
80
+ try {
81
+ await this.agent.connect();
82
+ this.reconnectAttempts = 0;
83
+ }
84
+ catch {
85
+ // Agent not available - Phase 1 operates without it
86
+ }
87
+ }
88
+ /** Periodic health check: verify ADB, reconnect agent if dropped. */
89
+ startHealthChecks() {
90
+ this.healthTimer = setInterval(async () => {
91
+ // Check ADB
92
+ try {
93
+ await this.adb.isConnected();
94
+ }
95
+ catch {
96
+ // ADB went away - not much we can do automatically
97
+ }
98
+ // Reset reconnect counter if agent is healthy
99
+ if (this.agent.isConnected()) {
100
+ this.reconnectAttempts = 0;
101
+ }
102
+ // Reconnect agent if it dropped
103
+ if (!this.agent.isConnected() && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
104
+ this.reconnectAttempts++;
105
+ await this.sleep(RECONNECT_DELAY_MS);
106
+ await this.tryConnectAgent();
107
+ }
108
+ }, HEALTH_CHECK_INTERVAL_MS);
109
+ // Don't keep the process alive just for health checks
110
+ if (this.healthTimer.unref) {
111
+ this.healthTimer.unref();
112
+ }
113
+ }
114
+ sleep(ms) {
115
+ return new Promise((resolve) => setTimeout(resolve, ms));
116
+ }
117
+ }
118
+ exports.ConnectionManager = ConnectionManager;
119
+ //# sourceMappingURL=connection-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logcat-parser.d.ts","sourceRoot":"","sources":["../../src/util/logcat-parser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,8CAA8C;AAC9C,MAAM,MAAM,QAAQ,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEzD,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAgBD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAsBnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAGjF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAGxE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,EAAE,CAG7E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Logcat Parser - Parses Android logcat output into structured entries.
4
+ *
5
+ * Supports the "threadtime" format which is the default for `adb logcat -v threadtime`.
6
+ * Format: "MM-DD HH:MM:SS.mmm PID TID LEVEL TAG: MESSAGE"
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parseLogcat = parseLogcat;
10
+ exports.filterByLevel = filterByLevel;
11
+ exports.filterByTag = filterByTag;
12
+ exports.filterByGrep = filterByGrep;
13
+ exports.formatEntry = formatEntry;
14
+ /** Regex for threadtime logcat format. */
15
+ const THREADTIME_RE = /^(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+(.+?):\s+(.*)$/;
16
+ /** Level priorities for filtering. */
17
+ const LEVEL_PRIORITY = {
18
+ V: 0,
19
+ D: 1,
20
+ I: 2,
21
+ W: 3,
22
+ E: 4,
23
+ F: 5,
24
+ };
25
+ /**
26
+ * Parse raw logcat output into structured entries.
27
+ *
28
+ * @param raw - Raw logcat text (threadtime format).
29
+ * @returns Array of parsed log entries.
30
+ */
31
+ function parseLogcat(raw) {
32
+ const lines = raw.split("\n");
33
+ const entries = [];
34
+ for (const line of lines) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed || trimmed.startsWith("-----"))
37
+ continue;
38
+ const match = trimmed.match(THREADTIME_RE);
39
+ if (match) {
40
+ entries.push({
41
+ timestamp: match[1],
42
+ pid: match[2],
43
+ tid: match[3],
44
+ level: match[4],
45
+ tag: match[5].trim(),
46
+ message: match[6],
47
+ });
48
+ }
49
+ }
50
+ return entries;
51
+ }
52
+ /**
53
+ * Filter log entries by minimum level.
54
+ */
55
+ function filterByLevel(entries, minLevel) {
56
+ const minPriority = LEVEL_PRIORITY[minLevel];
57
+ return entries.filter((e) => LEVEL_PRIORITY[e.level] >= minPriority);
58
+ }
59
+ /**
60
+ * Filter log entries by tag (case-insensitive substring match).
61
+ */
62
+ function filterByTag(entries, tag) {
63
+ const lower = tag.toLowerCase();
64
+ return entries.filter((e) => e.tag.toLowerCase().includes(lower));
65
+ }
66
+ /**
67
+ * Filter log entries by grep pattern (applied to message, case-insensitive).
68
+ */
69
+ function filterByGrep(entries, pattern) {
70
+ const re = new RegExp(pattern, "i");
71
+ return entries.filter((e) => re.test(e.message) || re.test(e.tag));
72
+ }
73
+ /**
74
+ * Format a log entry back to a human-readable string.
75
+ */
76
+ function formatEntry(entry) {
77
+ return `${entry.timestamp} ${entry.pid} ${entry.tid} ${entry.level} ${entry.tag}: ${entry.message}`;
78
+ }
79
+ //# sourceMappingURL=logcat-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/util/safety.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuDH,oCAAoC;AACpC,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAkCnE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CA+BxD"}
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ /**
3
+ * Safety - ADB command allowlist validation.
4
+ *
5
+ * Restricts which ADB commands can be executed through the `run_adb` tool
6
+ * to prevent destructive or dangerous operations on the device.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.validateAdbCommand = validateAdbCommand;
10
+ exports.parseCommandString = parseCommandString;
11
+ /**
12
+ * Allowed top-level ADB commands.
13
+ * These are the first argument after `adb -s <device>`.
14
+ */
15
+ const ALLOWED_ADB_COMMANDS = new Set([
16
+ "shell",
17
+ "pull",
18
+ "push",
19
+ "install",
20
+ "uninstall",
21
+ "logcat",
22
+ "bugreport",
23
+ "devices",
24
+ "get-state",
25
+ "get-serialno",
26
+ "forward",
27
+ "reverse",
28
+ "version",
29
+ ]);
30
+ /**
31
+ * Blocked shell sub-commands or patterns that could be destructive.
32
+ */
33
+ const BLOCKED_SHELL_PATTERNS = [
34
+ /^rm\s+-rf?\s+\/(?!sdcard|data\/local\/tmp)/i, // rm -rf outside safe dirs
35
+ /\bformat\b/i,
36
+ /\bdd\s+if=/i,
37
+ /\bflash\b/i,
38
+ /\breboot\s+bootloader/i,
39
+ /\breboot\b/i,
40
+ /\bfastboot\b/i,
41
+ /\bsu\s/i,
42
+ /\bsu$/i,
43
+ /\bmkfs\b/i,
44
+ /\bwipe\b/i,
45
+ /[;&|`]/, // Block shell chaining operators (;, &, |, backtick)
46
+ /\$\(/, // Block command substitution $()
47
+ ];
48
+ /**
49
+ * Blocked top-level ADB commands.
50
+ */
51
+ const BLOCKED_ADB_COMMANDS = new Set([
52
+ "root",
53
+ "remount",
54
+ "reboot-bootloader",
55
+ "sideload",
56
+ "restore",
57
+ "disable-verity",
58
+ "enable-verity",
59
+ "keygen",
60
+ ]);
61
+ /**
62
+ * Validate whether an ADB command is safe to execute.
63
+ *
64
+ * @param args - The ADB command arguments (after `adb -s <device>`).
65
+ * @returns Validation result with allowed flag and optional denial reason.
66
+ */
67
+ function validateAdbCommand(args) {
68
+ if (args.length === 0) {
69
+ return { allowed: false, reason: "Empty command" };
70
+ }
71
+ const command = args[0].toLowerCase();
72
+ // Check against blocked commands
73
+ if (BLOCKED_ADB_COMMANDS.has(command)) {
74
+ return { allowed: false, reason: `Command '${command}' is blocked for safety` };
75
+ }
76
+ // Check against allowlist
77
+ if (!ALLOWED_ADB_COMMANDS.has(command)) {
78
+ return {
79
+ allowed: false,
80
+ reason: `Command '${command}' is not in the allowlist. Allowed: ${[...ALLOWED_ADB_COMMANDS].join(", ")}`,
81
+ };
82
+ }
83
+ // Extra validation for shell commands
84
+ if (command === "shell" && args.length > 1) {
85
+ const shellCmd = args.slice(1).join(" ");
86
+ for (const pattern of BLOCKED_SHELL_PATTERNS) {
87
+ if (pattern.test(shellCmd)) {
88
+ return {
89
+ allowed: false,
90
+ reason: `Shell command matches blocked pattern: ${pattern.source}`,
91
+ };
92
+ }
93
+ }
94
+ }
95
+ return { allowed: true };
96
+ }
97
+ /**
98
+ * Parse a command string into an argument array, respecting quotes.
99
+ */
100
+ function parseCommandString(cmd) {
101
+ const args = [];
102
+ let current = "";
103
+ let inQuote = null;
104
+ for (let i = 0; i < cmd.length; i++) {
105
+ const ch = cmd[i];
106
+ if (inQuote) {
107
+ if (ch === inQuote) {
108
+ inQuote = null;
109
+ }
110
+ else {
111
+ current += ch;
112
+ }
113
+ }
114
+ else if (ch === '"' || ch === "'") {
115
+ inQuote = ch;
116
+ }
117
+ else if (ch === " " || ch === "\t") {
118
+ if (current) {
119
+ args.push(current);
120
+ current = "";
121
+ }
122
+ }
123
+ else {
124
+ current += ch;
125
+ }
126
+ }
127
+ if (current) {
128
+ args.push(current);
129
+ }
130
+ return args;
131
+ }
132
+ //# sourceMappingURL=safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safety.test.d.ts","sourceRoot":"","sources":["../../src/util/safety.test.ts"],"names":[],"mappings":""}