driftx 0.1.1 → 0.1.3
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 +216 -0
- package/dist/bin.js +834 -81
- package/dist/bin.js.map +1 -1
- package/driftx-plugin/skills/driftx/SKILL.md +281 -27
- package/ios-companion/DriftxCompanion/DriftxCompanionApp.swift +10 -0
- package/ios-companion/DriftxCompanion/Info.plist +22 -0
- package/ios-companion/DriftxCompanion.xcodeproj/project.pbxproj +376 -0
- package/ios-companion/DriftxCompanion.xcodeproj/xcshareddata/xcschemes/DriftxCompanionUITests.xcscheme +109 -0
- package/ios-companion/DriftxCompanionUITests/CompanionServer.swift +176 -0
- package/ios-companion/DriftxCompanionUITests/DriftxCompanionUITests.swift +15 -0
- package/ios-companion/DriftxCompanionUITests/HierarchyEndpoint.swift +140 -0
- package/ios-companion/DriftxCompanionUITests/Info.plist +22 -0
- package/ios-companion/DriftxCompanionUITests/InteractionEndpoint.swift +142 -0
- package/ios-companion/DriftxCompanionUITests/Router.swift +47 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanion.app/DriftxCompanion +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanion.app/DriftxCompanion.debug.dylib +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanion.app/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanion.app/PkgInfo +1 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanion.app/__preview.dylib +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/DriftxCompanionUITests-Runner +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/Testing.framework/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/Testing.framework/Testing +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/Testing.framework/_CodeSignature/CodeResources +168 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/Testing.framework/version.plist +18 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/XCTAutomationSupport +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/_CodeSignature/CodeResources +113 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/version.plist +18 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTest.framework/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTest.framework/XCTest +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTest.framework/_CodeSignature/CodeResources +817 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTest.framework/version.plist +18 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestCore.framework/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestCore.framework/XCTestCore +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestCore.framework/_CodeSignature/CodeResources +113 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestCore.framework/version.plist +18 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestSupport.framework/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestSupport.framework/XCTestSupport +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestSupport.framework/_CodeSignature/CodeResources +113 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCTestSupport.framework/version.plist +18 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUIAutomation.framework/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUIAutomation.framework/XCUIAutomation +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUIAutomation.framework/_CodeSignature/CodeResources +432 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUIAutomation.framework/version.plist +18 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUnit.framework/Info.plist +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUnit.framework/XCUnit +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUnit.framework/_CodeSignature/CodeResources +113 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/XCUnit.framework/version.plist +18 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Frameworks/libXCTestSwiftSupport.dylib +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/Info.plist +254 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/PkgInfo +1 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/PlugIns/DriftxCompanionUITests.xctest/DriftxCompanionUITests +0 -0
- package/ios-companion/prebuilt/Debug-iphonesimulator/DriftxCompanionUITests-Runner.app/PlugIns/DriftxCompanionUITests.xctest/Info.plist +0 -0
- package/ios-companion/prebuilt/DriftxCompanionUITests.xctestrun +135 -0
- package/ios-companion/prebuilt/build-info.json +6 -0
- package/package.json +14 -3
package/dist/bin.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
-
import { existsSync as
|
|
7
|
-
import { dirname as
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
6
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, readdirSync as readdirSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
7
|
+
import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
|
|
8
|
+
import { homedir as homedir2 } from "os";
|
|
9
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
10
|
|
|
11
11
|
// src/shell.ts
|
|
12
12
|
import { execFile } from "child_process";
|
|
@@ -14,7 +14,7 @@ var RealShell = class {
|
|
|
14
14
|
async exec(cmd, args, options) {
|
|
15
15
|
const controller = new AbortController();
|
|
16
16
|
const timeout = options?.timeout;
|
|
17
|
-
return new Promise((
|
|
17
|
+
return new Promise((resolve3, reject) => {
|
|
18
18
|
const child = execFile(
|
|
19
19
|
cmd,
|
|
20
20
|
args,
|
|
@@ -27,7 +27,7 @@ var RealShell = class {
|
|
|
27
27
|
if (error) {
|
|
28
28
|
reject(error);
|
|
29
29
|
} else {
|
|
30
|
-
|
|
30
|
+
resolve3({ stdout, stderr });
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
);
|
|
@@ -120,6 +120,7 @@ var configSchema = z.object({
|
|
|
120
120
|
regionMergeGap: z.number().int().nonnegative().optional(),
|
|
121
121
|
regionMinArea: z.number().int().nonnegative().optional(),
|
|
122
122
|
diffMaskColor: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
|
|
123
|
+
companionPort: z.number().int().positive().optional(),
|
|
123
124
|
analyses: analysesSchema
|
|
124
125
|
});
|
|
125
126
|
var DEFAULTS = {
|
|
@@ -145,6 +146,7 @@ var DEFAULTS = {
|
|
|
145
146
|
regionMergeGap: 8,
|
|
146
147
|
regionMinArea: 100,
|
|
147
148
|
diffMaskColor: [255, 0, 0, 128],
|
|
149
|
+
companionPort: 8300,
|
|
148
150
|
analyses: {
|
|
149
151
|
default: [],
|
|
150
152
|
disabled: [],
|
|
@@ -160,6 +162,7 @@ function parseConfig(raw) {
|
|
|
160
162
|
return {
|
|
161
163
|
...defaults,
|
|
162
164
|
...parsed,
|
|
165
|
+
companionPort: parsed.companionPort ?? defaults.companionPort,
|
|
163
166
|
viewport: { ...defaults.viewport, ...parsed.viewport },
|
|
164
167
|
timeouts: { ...defaults.timeouts, ...parsed.timeouts },
|
|
165
168
|
retry: { ...defaults.retry, ...parsed.retry },
|
|
@@ -249,14 +252,14 @@ async function checkOne(spec, shell) {
|
|
|
249
252
|
}
|
|
250
253
|
async function checkMetro(port) {
|
|
251
254
|
try {
|
|
252
|
-
const { default:
|
|
253
|
-
const status = await new Promise((
|
|
254
|
-
const req =
|
|
255
|
+
const { default: http3 } = await import("http");
|
|
256
|
+
const status = await new Promise((resolve3, reject) => {
|
|
257
|
+
const req = http3.get(`http://localhost:${port}/status`, { timeout: 2e3 }, (res) => {
|
|
255
258
|
let data = "";
|
|
256
259
|
res.on("data", (chunk) => {
|
|
257
260
|
data += chunk;
|
|
258
261
|
});
|
|
259
|
-
res.on("end", () =>
|
|
262
|
+
res.on("end", () => resolve3(data));
|
|
260
263
|
});
|
|
261
264
|
req.on("error", reject);
|
|
262
265
|
req.on("timeout", () => {
|
|
@@ -523,7 +526,7 @@ function isScreenSettled(buf1, buf2, maxDelta) {
|
|
|
523
526
|
return diffPixels / totalPixels <= maxDelta;
|
|
524
527
|
}
|
|
525
528
|
function delay(ms) {
|
|
526
|
-
return new Promise((
|
|
529
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
527
530
|
}
|
|
528
531
|
async function captureScreenshot(shell, device, options) {
|
|
529
532
|
const logger = getLogger();
|
|
@@ -622,12 +625,12 @@ function isRetryable(error, policy) {
|
|
|
622
625
|
return policy.retryableErrors.some((pattern) => error.message.includes(pattern));
|
|
623
626
|
}
|
|
624
627
|
function delay2(ms, signal) {
|
|
625
|
-
return new Promise((
|
|
628
|
+
return new Promise((resolve3, reject) => {
|
|
626
629
|
if (signal?.aborted) {
|
|
627
630
|
reject(new Error("Aborted"));
|
|
628
631
|
return;
|
|
629
632
|
}
|
|
630
|
-
const timer = setTimeout(
|
|
633
|
+
const timer = setTimeout(resolve3, ms);
|
|
631
634
|
signal?.addEventListener("abort", () => {
|
|
632
635
|
clearTimeout(timer);
|
|
633
636
|
reject(new Error("Aborted"));
|
|
@@ -908,8 +911,133 @@ var FIBER_WALK_SCRIPT = `(function() {
|
|
|
908
911
|
}
|
|
909
912
|
}
|
|
910
913
|
})()`;
|
|
914
|
+
var MEASURE_FIND_SCRIPT = (text) => `(function() {
|
|
915
|
+
try {
|
|
916
|
+
globalThis.__driftx_measure = null;
|
|
917
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
918
|
+
if (!hook || !hook.renderers) return JSON.stringify({error: "no hook"});
|
|
919
|
+
var targetText = ${JSON.stringify(text)};
|
|
920
|
+
var found = null;
|
|
921
|
+
function searchFiber(fiber) {
|
|
922
|
+
if (!fiber || found) return;
|
|
923
|
+
var props = fiber.memoizedProps || {};
|
|
924
|
+
if (typeof props.children === 'string' && props.children === targetText) { found = fiber; return; }
|
|
925
|
+
var child = fiber.child;
|
|
926
|
+
while (child) { searchFiber(child); child = child.sibling; }
|
|
927
|
+
}
|
|
928
|
+
hook.renderers.forEach(function(renderer, id) {
|
|
929
|
+
var roots = hook.getFiberRoots(id);
|
|
930
|
+
if (!roots) return;
|
|
931
|
+
roots.forEach(function(root) { if (root.current) searchFiber(root.current); });
|
|
932
|
+
});
|
|
933
|
+
if (!found) return JSON.stringify({error: "not found"});
|
|
934
|
+
var host = found;
|
|
935
|
+
while (host) {
|
|
936
|
+
if (host.tag === 5 && host.stateNode != null) break;
|
|
937
|
+
host = host.return;
|
|
938
|
+
}
|
|
939
|
+
if (!host || !host.stateNode) return JSON.stringify({error: "no host"});
|
|
940
|
+
var sn = host.stateNode;
|
|
941
|
+
if (typeof sn.measureInWindow === 'function') {
|
|
942
|
+
sn.measureInWindow(function(x, y, w, h) {
|
|
943
|
+
globalThis.__driftx_measure = {x: x, y: y, width: w, height: h};
|
|
944
|
+
});
|
|
945
|
+
return JSON.stringify({status: "measuring"});
|
|
946
|
+
}
|
|
947
|
+
return JSON.stringify({error: "no measureInWindow"});
|
|
948
|
+
} catch(e) { return JSON.stringify({error: e.message}); }
|
|
949
|
+
})()`;
|
|
950
|
+
var MEASURE_READ_SCRIPT = `(function() {
|
|
951
|
+
var r = globalThis.__driftx_measure;
|
|
952
|
+
globalThis.__driftx_measure = null;
|
|
953
|
+
return r ? JSON.stringify(r) : JSON.stringify(null);
|
|
954
|
+
})()`;
|
|
955
|
+
async function measureElementByText(metroPort, text, deviceName, timeoutMs = 1e4) {
|
|
956
|
+
const logger = getLogger();
|
|
957
|
+
const targets = await discoverTargets(metroPort);
|
|
958
|
+
const target = findRuntimeTarget(targets, deviceName);
|
|
959
|
+
if (!target) return null;
|
|
960
|
+
return new Promise((resolve3) => {
|
|
961
|
+
const timer = setTimeout(() => {
|
|
962
|
+
cleanup();
|
|
963
|
+
resolve3(null);
|
|
964
|
+
}, timeoutMs);
|
|
965
|
+
let ws;
|
|
966
|
+
function cleanup() {
|
|
967
|
+
try {
|
|
968
|
+
ws?.close();
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
974
|
+
} catch {
|
|
975
|
+
clearTimeout(timer);
|
|
976
|
+
return resolve3(null);
|
|
977
|
+
}
|
|
978
|
+
ws.on("open", () => {
|
|
979
|
+
ws.send(JSON.stringify({
|
|
980
|
+
id: 1,
|
|
981
|
+
method: "Runtime.evaluate",
|
|
982
|
+
params: { expression: MEASURE_FIND_SCRIPT(text), returnByValue: true }
|
|
983
|
+
}));
|
|
984
|
+
});
|
|
985
|
+
ws.on("message", (data) => {
|
|
986
|
+
try {
|
|
987
|
+
const msg = JSON.parse(data.toString());
|
|
988
|
+
if (msg.id === 1) {
|
|
989
|
+
const val = msg.result?.result?.value;
|
|
990
|
+
if (typeof val === "string") {
|
|
991
|
+
const parsed = JSON.parse(val);
|
|
992
|
+
if (parsed?.status === "measuring") {
|
|
993
|
+
setTimeout(() => {
|
|
994
|
+
ws.send(JSON.stringify({
|
|
995
|
+
id: 2,
|
|
996
|
+
method: "Runtime.evaluate",
|
|
997
|
+
params: { expression: MEASURE_READ_SCRIPT, returnByValue: true }
|
|
998
|
+
}));
|
|
999
|
+
}, 200);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
clearTimeout(timer);
|
|
1004
|
+
cleanup();
|
|
1005
|
+
resolve3(null);
|
|
1006
|
+
}
|
|
1007
|
+
if (msg.id === 2) {
|
|
1008
|
+
clearTimeout(timer);
|
|
1009
|
+
const val = msg.result?.result?.value;
|
|
1010
|
+
if (typeof val === "string") {
|
|
1011
|
+
const parsed = JSON.parse(val);
|
|
1012
|
+
if (parsed && typeof parsed.x === "number" && parsed.width > 0) {
|
|
1013
|
+
logger.debug(`CDP measure '${text}': ${JSON.stringify(parsed)}`);
|
|
1014
|
+
cleanup();
|
|
1015
|
+
return resolve3(parsed);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
cleanup();
|
|
1019
|
+
resolve3(null);
|
|
1020
|
+
}
|
|
1021
|
+
} catch {
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
ws.on("error", () => {
|
|
1025
|
+
clearTimeout(timer);
|
|
1026
|
+
cleanup();
|
|
1027
|
+
resolve3(null);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
async function detectBundleId(metroPort, deviceName) {
|
|
1032
|
+
const targets = await discoverTargets(metroPort);
|
|
1033
|
+
const target = findRuntimeTarget(targets, deviceName);
|
|
1034
|
+
if (!target) return null;
|
|
1035
|
+
if (target.appId) return target.appId;
|
|
1036
|
+
if (target.description && /^[a-zA-Z][a-zA-Z0-9._-]+$/.test(target.description)) return target.description;
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
911
1039
|
async function discoverTargets(metroPort) {
|
|
912
|
-
return new Promise((
|
|
1040
|
+
return new Promise((resolve3, reject) => {
|
|
913
1041
|
const req = http.get(`http://localhost:${metroPort}/json/list`, { timeout: 2e3 }, (res) => {
|
|
914
1042
|
let data = "";
|
|
915
1043
|
res.on("data", (chunk) => {
|
|
@@ -917,16 +1045,16 @@ async function discoverTargets(metroPort) {
|
|
|
917
1045
|
});
|
|
918
1046
|
res.on("end", () => {
|
|
919
1047
|
try {
|
|
920
|
-
|
|
1048
|
+
resolve3(JSON.parse(data));
|
|
921
1049
|
} catch {
|
|
922
|
-
|
|
1050
|
+
resolve3([]);
|
|
923
1051
|
}
|
|
924
1052
|
});
|
|
925
1053
|
});
|
|
926
|
-
req.on("error", () =>
|
|
1054
|
+
req.on("error", () => resolve3([]));
|
|
927
1055
|
req.on("timeout", () => {
|
|
928
1056
|
req.destroy();
|
|
929
|
-
|
|
1057
|
+
resolve3([]);
|
|
930
1058
|
});
|
|
931
1059
|
});
|
|
932
1060
|
}
|
|
@@ -953,16 +1081,16 @@ var CdpClient = class {
|
|
|
953
1081
|
return [];
|
|
954
1082
|
}
|
|
955
1083
|
logger.debug(`Found CDP target: ${target.title} (${target.description})`);
|
|
956
|
-
return new Promise((
|
|
1084
|
+
return new Promise((resolve3, reject) => {
|
|
957
1085
|
const timer = setTimeout(() => {
|
|
958
1086
|
this.cleanup();
|
|
959
|
-
|
|
1087
|
+
resolve3([]);
|
|
960
1088
|
}, timeoutMs);
|
|
961
1089
|
try {
|
|
962
1090
|
this.ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
963
1091
|
} catch {
|
|
964
1092
|
clearTimeout(timer);
|
|
965
|
-
|
|
1093
|
+
resolve3([]);
|
|
966
1094
|
return;
|
|
967
1095
|
}
|
|
968
1096
|
this.ws.on("open", async () => {
|
|
@@ -970,10 +1098,10 @@ var CdpClient = class {
|
|
|
970
1098
|
try {
|
|
971
1099
|
const result = await this.evaluate(FIBER_WALK_SCRIPT);
|
|
972
1100
|
clearTimeout(timer);
|
|
973
|
-
|
|
1101
|
+
resolve3(this.parseResult(result));
|
|
974
1102
|
} catch {
|
|
975
1103
|
clearTimeout(timer);
|
|
976
|
-
|
|
1104
|
+
resolve3([]);
|
|
977
1105
|
}
|
|
978
1106
|
});
|
|
979
1107
|
this.ws.on("message", (data) => {
|
|
@@ -991,7 +1119,7 @@ var CdpClient = class {
|
|
|
991
1119
|
});
|
|
992
1120
|
this.ws.on("error", () => {
|
|
993
1121
|
clearTimeout(timer);
|
|
994
|
-
|
|
1122
|
+
resolve3([]);
|
|
995
1123
|
});
|
|
996
1124
|
this.ws.on("close", () => {
|
|
997
1125
|
this.connected = false;
|
|
@@ -1000,8 +1128,8 @@ var CdpClient = class {
|
|
|
1000
1128
|
}
|
|
1001
1129
|
evaluate(expression) {
|
|
1002
1130
|
const id = ++this.msgId;
|
|
1003
|
-
return new Promise((
|
|
1004
|
-
this.pending.set(id, { resolve:
|
|
1131
|
+
return new Promise((resolve3, reject) => {
|
|
1132
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
1005
1133
|
this.ws.send(JSON.stringify({
|
|
1006
1134
|
id,
|
|
1007
1135
|
method: "Runtime.evaluate",
|
|
@@ -1168,13 +1296,56 @@ var StrategyCache = class {
|
|
|
1168
1296
|
}
|
|
1169
1297
|
};
|
|
1170
1298
|
|
|
1299
|
+
// src/ios-companion/hierarchy-parser.ts
|
|
1300
|
+
function cleanElementType(elementType) {
|
|
1301
|
+
const prefix = "XCUIElementType";
|
|
1302
|
+
if (elementType.startsWith(prefix)) {
|
|
1303
|
+
return elementType.slice(prefix.length);
|
|
1304
|
+
}
|
|
1305
|
+
return elementType.charAt(0).toUpperCase() + elementType.slice(1);
|
|
1306
|
+
}
|
|
1307
|
+
function isZeroSize(frame) {
|
|
1308
|
+
return frame.width === 0 && frame.height === 0;
|
|
1309
|
+
}
|
|
1310
|
+
function convertNode(node, idCounter) {
|
|
1311
|
+
const children = [];
|
|
1312
|
+
for (const child of node.children) {
|
|
1313
|
+
const converted = convertNode(child, idCounter);
|
|
1314
|
+
if (converted) children.push(converted);
|
|
1315
|
+
}
|
|
1316
|
+
if (children.length === 0 && isZeroSize(node.frame)) {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
id: String(idCounter.n++),
|
|
1321
|
+
name: cleanElementType(node.elementType),
|
|
1322
|
+
nativeName: node.elementType,
|
|
1323
|
+
testID: node.identifier || void 0,
|
|
1324
|
+
bounds: { x: node.frame.x, y: node.frame.y, width: node.frame.width, height: node.frame.height },
|
|
1325
|
+
text: node.label || void 0,
|
|
1326
|
+
children,
|
|
1327
|
+
inspectionTier: "basic"
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function parseCompanionHierarchy(nodes) {
|
|
1331
|
+
const idCounter = { n: 0 };
|
|
1332
|
+
const result = [];
|
|
1333
|
+
for (const node of nodes) {
|
|
1334
|
+
const converted = convertNode(node, idCounter);
|
|
1335
|
+
if (converted) result.push(converted);
|
|
1336
|
+
}
|
|
1337
|
+
return result;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1171
1340
|
// src/inspect/tree-inspector.ts
|
|
1172
1341
|
var TreeInspector = class {
|
|
1173
1342
|
shell;
|
|
1174
1343
|
fileCache;
|
|
1175
|
-
|
|
1344
|
+
companionLauncher;
|
|
1345
|
+
constructor(shell, projectRoot, companionLauncher) {
|
|
1176
1346
|
this.shell = shell;
|
|
1177
1347
|
this.fileCache = projectRoot ? new StrategyCache(projectRoot) : null;
|
|
1348
|
+
this.companionLauncher = companionLauncher ?? null;
|
|
1178
1349
|
}
|
|
1179
1350
|
invalidateCache(deviceId) {
|
|
1180
1351
|
if (!this.fileCache) return;
|
|
@@ -1200,6 +1371,9 @@ var TreeInspector = class {
|
|
|
1200
1371
|
if (device.platform === "android") {
|
|
1201
1372
|
return { method: "uiautomator", reason: "Android native inspection" };
|
|
1202
1373
|
}
|
|
1374
|
+
if (this.companionLauncher) {
|
|
1375
|
+
return { method: "xcuitest", reason: "iOS native inspection via XCUITest companion" };
|
|
1376
|
+
}
|
|
1203
1377
|
return { method: "idb", reason: "iOS native inspection via idb" };
|
|
1204
1378
|
}
|
|
1205
1379
|
async inspect(device, options) {
|
|
@@ -1257,13 +1431,30 @@ var TreeInspector = class {
|
|
|
1257
1431
|
logger.debug(`UIAutomator failed: ${err instanceof Error ? err.message : err}`);
|
|
1258
1432
|
}
|
|
1259
1433
|
}
|
|
1260
|
-
if (strategy.method === "
|
|
1434
|
+
if (strategy.method === "xcuitest" || strategy.method === "cdp" && device.platform === "ios" && this.companionLauncher) {
|
|
1435
|
+
try {
|
|
1436
|
+
const client = await this.companionLauncher.ensureRunning(device.id, options.bundleId);
|
|
1437
|
+
const rawNodes = await client.hierarchy();
|
|
1438
|
+
const tree = parseCompanionHierarchy(rawNodes);
|
|
1439
|
+
logger.debug(`xcuitest: got ${tree.length} root nodes for ${device.name}`);
|
|
1440
|
+
return {
|
|
1441
|
+
...base,
|
|
1442
|
+
strategy: { method: "xcuitest", reason: strategy.method === "cdp" ? "CDP fallback to native" : strategy.reason },
|
|
1443
|
+
tree,
|
|
1444
|
+
capabilities: { tree: "basic", sourceMapping: "none", styles: "none", protocol: "xcuitest" },
|
|
1445
|
+
hints
|
|
1446
|
+
};
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
logger.debug(`XCUITest companion failed: ${err instanceof Error ? err.message : err}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (strategy.method === "idb" || strategy.method === "cdp" && device.platform === "ios" || strategy.method === "xcuitest") {
|
|
1261
1452
|
try {
|
|
1262
1453
|
const tree = await dumpIosAccessibility(this.shell, device.id, options.timeoutMs);
|
|
1263
1454
|
logger.debug(`idb: got ${tree.length} root nodes for ${device.name}`);
|
|
1264
1455
|
return {
|
|
1265
1456
|
...base,
|
|
1266
|
-
strategy: { method: "idb", reason: strategy.method === "cdp" ? "CDP fallback to native" : strategy.reason },
|
|
1457
|
+
strategy: { method: "idb", reason: strategy.method === "xcuitest" ? "XCUITest fallback to idb" : strategy.method === "cdp" ? "CDP fallback to native" : strategy.reason },
|
|
1267
1458
|
tree,
|
|
1268
1459
|
capabilities: { tree: "basic", sourceMapping: "none", styles: "none", protocol: "idb" },
|
|
1269
1460
|
hints
|
|
@@ -2323,12 +2514,12 @@ async function copyToClipboard(text) {
|
|
|
2323
2514
|
logger.debug(`Clipboard not supported on ${process.platform}`);
|
|
2324
2515
|
return;
|
|
2325
2516
|
}
|
|
2326
|
-
return new Promise((
|
|
2517
|
+
return new Promise((resolve3) => {
|
|
2327
2518
|
const proc = exec(cmd, (err) => {
|
|
2328
2519
|
if (err) {
|
|
2329
2520
|
logger.debug(`Clipboard copy failed: ${err.message}`);
|
|
2330
2521
|
}
|
|
2331
|
-
|
|
2522
|
+
resolve3();
|
|
2332
2523
|
});
|
|
2333
2524
|
proc.stdin?.write(text);
|
|
2334
2525
|
proc.stdin?.end();
|
|
@@ -2568,35 +2759,35 @@ var AndroidBackend = class {
|
|
|
2568
2759
|
|
|
2569
2760
|
// src/interact/ios.ts
|
|
2570
2761
|
var IosBackend = class {
|
|
2571
|
-
constructor(shell) {
|
|
2762
|
+
constructor(shell, companion) {
|
|
2572
2763
|
this.shell = shell;
|
|
2764
|
+
this.companion = companion;
|
|
2573
2765
|
}
|
|
2574
|
-
async tap(
|
|
2575
|
-
await this.
|
|
2766
|
+
async tap(_device, point) {
|
|
2767
|
+
await this.companion.tap(point.x, point.y);
|
|
2576
2768
|
}
|
|
2577
|
-
async longPress(
|
|
2578
|
-
await this.
|
|
2769
|
+
async longPress(_device, point, durationMs) {
|
|
2770
|
+
await this.companion.longPress(point.x, point.y, durationMs);
|
|
2579
2771
|
}
|
|
2580
|
-
async swipe(
|
|
2581
|
-
await this.
|
|
2772
|
+
async swipe(_device, from, to, durationMs) {
|
|
2773
|
+
await this.companion.swipe(from.x, from.y, to.x, to.y, durationMs);
|
|
2582
2774
|
}
|
|
2583
|
-
async type(
|
|
2584
|
-
await this.
|
|
2775
|
+
async type(_device, text) {
|
|
2776
|
+
await this.companion.type(text);
|
|
2585
2777
|
}
|
|
2586
|
-
async keyEvent(
|
|
2587
|
-
await this.
|
|
2778
|
+
async keyEvent(_device, key) {
|
|
2779
|
+
await this.companion.keyEvent(key);
|
|
2588
2780
|
}
|
|
2589
2781
|
async openUrl(device, url) {
|
|
2590
2782
|
await this.shell.exec("xcrun", ["simctl", "openurl", device.id, url]);
|
|
2591
2783
|
}
|
|
2592
|
-
async simctlIo(device, args) {
|
|
2593
|
-
await this.shell.exec("xcrun", ["simctl", "io", device.id, ...args]);
|
|
2594
|
-
}
|
|
2595
2784
|
};
|
|
2596
2785
|
|
|
2597
2786
|
// src/interact/backend.ts
|
|
2598
|
-
function createBackend(shell, platform) {
|
|
2599
|
-
|
|
2787
|
+
function createBackend(shell, platform, companion) {
|
|
2788
|
+
if (platform === "android") return new AndroidBackend(shell);
|
|
2789
|
+
if (!companion) throw new Error("iOS interactions require the XCUITest companion. Run on a simulator with Xcode installed.");
|
|
2790
|
+
return new IosBackend(shell, companion);
|
|
2600
2791
|
}
|
|
2601
2792
|
|
|
2602
2793
|
// src/interact/resolver.ts
|
|
@@ -2608,6 +2799,17 @@ function resolveTarget(tree, query) {
|
|
|
2608
2799
|
if (byName) return centerOf(byName, `name:${query}`);
|
|
2609
2800
|
const byText = nodes.find((n) => n.text === query && hasSize(n));
|
|
2610
2801
|
if (byText) return centerOf(byText, `text:${query}`);
|
|
2802
|
+
const lowerQuery = query.toLowerCase();
|
|
2803
|
+
const startsWithMatches = nodes.filter(
|
|
2804
|
+
(n) => n.text && hasSize(n) && (n.text.toLowerCase().startsWith(lowerQuery + ",") || n.text.toLowerCase().startsWith(lowerQuery + " "))
|
|
2805
|
+
);
|
|
2806
|
+
const byTextStartsWith = smallest(startsWithMatches);
|
|
2807
|
+
if (byTextStartsWith) return centerOf(byTextStartsWith, `text~:${query}`);
|
|
2808
|
+
const containsMatches = nodes.filter(
|
|
2809
|
+
(n) => n.text && hasSize(n) && n.text.toLowerCase().includes(lowerQuery)
|
|
2810
|
+
);
|
|
2811
|
+
const byTextContains = smallest(containsMatches);
|
|
2812
|
+
if (byTextContains) return centerOf(byTextContains, `text~:${query}`);
|
|
2611
2813
|
return null;
|
|
2612
2814
|
}
|
|
2613
2815
|
function flattenTree(nodes) {
|
|
@@ -2623,6 +2825,13 @@ function flattenTree(nodes) {
|
|
|
2623
2825
|
function hasSize(node) {
|
|
2624
2826
|
return node.bounds.width > 0 && node.bounds.height > 0;
|
|
2625
2827
|
}
|
|
2828
|
+
function area(node) {
|
|
2829
|
+
return node.bounds.width * node.bounds.height;
|
|
2830
|
+
}
|
|
2831
|
+
function smallest(nodes) {
|
|
2832
|
+
if (nodes.length === 0) return void 0;
|
|
2833
|
+
return nodes.reduce((best, n) => area(n) < area(best) ? n : best);
|
|
2834
|
+
}
|
|
2626
2835
|
function centerOf(node, resolvedFrom) {
|
|
2627
2836
|
return {
|
|
2628
2837
|
x: Math.round(node.bounds.x + node.bounds.width / 2),
|
|
@@ -2672,13 +2881,15 @@ var GestureExecutor = class {
|
|
|
2672
2881
|
return { success: false, action: "longPress", durationMs: Date.now() - start, error: String(e) };
|
|
2673
2882
|
}
|
|
2674
2883
|
}
|
|
2675
|
-
async swipe(device, direction, distance
|
|
2884
|
+
async swipe(device, direction, distance, durationMs = 300) {
|
|
2676
2885
|
const start = Date.now();
|
|
2677
2886
|
try {
|
|
2678
|
-
const
|
|
2679
|
-
const
|
|
2887
|
+
const isIos = device.platform === "ios";
|
|
2888
|
+
const cx = device.screenSize?.width ? Math.round(device.screenSize.width / 2) : isIos ? 197 : 540;
|
|
2889
|
+
const cy = device.screenSize?.height ? Math.round(device.screenSize.height / 2) : isIos ? 426 : 960;
|
|
2890
|
+
const actualDistance = distance ?? (isIos ? 300 : 600);
|
|
2680
2891
|
const from = { x: cx, y: cy };
|
|
2681
|
-
const half = Math.round(
|
|
2892
|
+
const half = Math.round(actualDistance / 2);
|
|
2682
2893
|
const to = direction === "up" ? { x: cx, y: cy - half } : direction === "down" ? { x: cx, y: cy + half } : direction === "left" ? { x: cx - half, y: cy } : { x: cx + half, y: cy };
|
|
2683
2894
|
await this.backend.swipe(device, from, to, durationMs);
|
|
2684
2895
|
return { success: true, action: "swipe", durationMs: Date.now() - start };
|
|
@@ -2721,6 +2932,470 @@ var GestureExecutor = class {
|
|
|
2721
2932
|
}
|
|
2722
2933
|
};
|
|
2723
2934
|
|
|
2935
|
+
// src/ios-companion/launcher.ts
|
|
2936
|
+
import { spawn } from "child_process";
|
|
2937
|
+
import { resolve, dirname as dirname4, join as join5 } from "path";
|
|
2938
|
+
import { fileURLToPath } from "url";
|
|
2939
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
2940
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2941
|
+
|
|
2942
|
+
// src/ios-companion/client.ts
|
|
2943
|
+
import http2 from "http";
|
|
2944
|
+
var CompanionClient = class {
|
|
2945
|
+
constructor(port) {
|
|
2946
|
+
this.port = port;
|
|
2947
|
+
}
|
|
2948
|
+
async status() {
|
|
2949
|
+
return this.request("GET", "/status");
|
|
2950
|
+
}
|
|
2951
|
+
async configure(bundleId) {
|
|
2952
|
+
await this.request("POST", "/configure", { bundleId }, 3e4);
|
|
2953
|
+
}
|
|
2954
|
+
async tap(x, y) {
|
|
2955
|
+
await this.request("POST", "/tap", { x, y });
|
|
2956
|
+
}
|
|
2957
|
+
async longPress(x, y, durationMs) {
|
|
2958
|
+
await this.request("POST", "/longPress", { x, y, durationMs });
|
|
2959
|
+
}
|
|
2960
|
+
async swipe(fromX, fromY, toX, toY, durationMs) {
|
|
2961
|
+
await this.request("POST", "/swipe", { fromX, fromY, toX, toY, durationMs });
|
|
2962
|
+
}
|
|
2963
|
+
async type(text) {
|
|
2964
|
+
await this.request("POST", "/type", { text });
|
|
2965
|
+
}
|
|
2966
|
+
async keyEvent(key) {
|
|
2967
|
+
await this.request("POST", "/keyEvent", { key });
|
|
2968
|
+
}
|
|
2969
|
+
async find(text) {
|
|
2970
|
+
try {
|
|
2971
|
+
const res = await this.request("POST", "/find", { text }, 3e4);
|
|
2972
|
+
const frame = res.frame;
|
|
2973
|
+
return frame ?? null;
|
|
2974
|
+
} catch {
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
async hierarchy() {
|
|
2979
|
+
return this.requestRaw("GET", "/hierarchy", void 0, 3e4);
|
|
2980
|
+
}
|
|
2981
|
+
requestRaw(method, path6, body, timeoutMs = 1e4) {
|
|
2982
|
+
return new Promise((resolve3, reject) => {
|
|
2983
|
+
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
2984
|
+
const req = http2.request(
|
|
2985
|
+
{
|
|
2986
|
+
hostname: "localhost",
|
|
2987
|
+
port: this.port,
|
|
2988
|
+
path: path6,
|
|
2989
|
+
method,
|
|
2990
|
+
timeout: timeoutMs,
|
|
2991
|
+
headers: payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : void 0
|
|
2992
|
+
},
|
|
2993
|
+
(res) => {
|
|
2994
|
+
let data = "";
|
|
2995
|
+
res.on("data", (chunk) => {
|
|
2996
|
+
data += chunk;
|
|
2997
|
+
});
|
|
2998
|
+
res.on("end", () => {
|
|
2999
|
+
if (res.statusCode !== 200) {
|
|
3000
|
+
reject(new Error(`Companion ${method} ${path6} returned ${res.statusCode}: ${data}`));
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
try {
|
|
3004
|
+
resolve3(JSON.parse(data));
|
|
3005
|
+
} catch {
|
|
3006
|
+
reject(new Error(`Companion ${method} ${path6} returned invalid JSON: ${data}`));
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
);
|
|
3011
|
+
req.on("error", (err) => reject(new Error(`Companion ${method} ${path6}: ${err.message}`)));
|
|
3012
|
+
req.on("timeout", () => {
|
|
3013
|
+
req.destroy();
|
|
3014
|
+
reject(new Error(`Companion ${method} ${path6} timed out after ${timeoutMs}ms`));
|
|
3015
|
+
});
|
|
3016
|
+
if (payload) req.write(payload);
|
|
3017
|
+
req.end();
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
request(method, path6, body, timeoutMs = 3e4) {
|
|
3021
|
+
return new Promise((resolve3, reject) => {
|
|
3022
|
+
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
3023
|
+
const req = http2.request(
|
|
3024
|
+
{
|
|
3025
|
+
hostname: "localhost",
|
|
3026
|
+
port: this.port,
|
|
3027
|
+
path: path6,
|
|
3028
|
+
method,
|
|
3029
|
+
timeout: timeoutMs,
|
|
3030
|
+
headers: payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : void 0
|
|
3031
|
+
},
|
|
3032
|
+
(res) => {
|
|
3033
|
+
let data = "";
|
|
3034
|
+
res.on("data", (chunk) => {
|
|
3035
|
+
data += chunk;
|
|
3036
|
+
});
|
|
3037
|
+
res.on("end", () => {
|
|
3038
|
+
if (res.statusCode !== 200) {
|
|
3039
|
+
reject(new Error(`Companion ${method} ${path6} returned ${res.statusCode}: ${data}`));
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
let parsed;
|
|
3043
|
+
try {
|
|
3044
|
+
parsed = JSON.parse(data);
|
|
3045
|
+
} catch {
|
|
3046
|
+
reject(new Error(`Companion ${method} ${path6} returned invalid JSON: ${data}`));
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
if (parsed.success === false) {
|
|
3050
|
+
reject(new Error(`Companion ${method} ${path6} failed: ${parsed.error ?? "unknown error"}`));
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
resolve3(parsed);
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
);
|
|
3057
|
+
req.on("error", (err) => reject(new Error(`Companion ${method} ${path6}: ${err.message}`)));
|
|
3058
|
+
req.on("timeout", () => {
|
|
3059
|
+
req.destroy();
|
|
3060
|
+
reject(new Error(`Companion ${method} ${path6} timed out after ${timeoutMs}ms`));
|
|
3061
|
+
});
|
|
3062
|
+
if (payload) req.write(payload);
|
|
3063
|
+
req.end();
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
|
|
3068
|
+
// src/ios-companion/xctestrun.ts
|
|
3069
|
+
function rewriteXctestrun(content, prebuiltDir, port) {
|
|
3070
|
+
let result = content;
|
|
3071
|
+
const rootMatch = result.match(/((?:__TESTROOT__|[^<\s]+?))\/Debug-/);
|
|
3072
|
+
if (rootMatch) {
|
|
3073
|
+
const originalRoot = rootMatch[1];
|
|
3074
|
+
const escaped = originalRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3075
|
+
result = result.replace(new RegExp(escaped, "g"), prebuiltDir);
|
|
3076
|
+
}
|
|
3077
|
+
const envTag = "<key>TestingEnvironmentVariables</key>";
|
|
3078
|
+
const envIdx = result.indexOf(envTag);
|
|
3079
|
+
if (envIdx !== -1) {
|
|
3080
|
+
const dictOpenIdx = result.indexOf("<dict>", envIdx);
|
|
3081
|
+
if (dictOpenIdx !== -1) {
|
|
3082
|
+
const insertPos = dictOpenIdx + "<dict>".length;
|
|
3083
|
+
const portEntry = `
|
|
3084
|
+
<key>DRIFTX_PORT</key>
|
|
3085
|
+
<string>${port}</string>`;
|
|
3086
|
+
result = result.slice(0, insertPos) + portEntry + result.slice(insertPos);
|
|
3087
|
+
}
|
|
3088
|
+
} else {
|
|
3089
|
+
const testConfigKey = "<key>DriftxCompanionUITests</key>";
|
|
3090
|
+
const configIdx = result.indexOf(testConfigKey);
|
|
3091
|
+
if (configIdx !== -1) {
|
|
3092
|
+
const closingDict = result.indexOf("</dict>", configIdx);
|
|
3093
|
+
if (closingDict !== -1) {
|
|
3094
|
+
const envBlock = ` <key>TestingEnvironmentVariables</key>
|
|
3095
|
+
<dict>
|
|
3096
|
+
<key>DRIFTX_PORT</key>
|
|
3097
|
+
<string>${port}</string>
|
|
3098
|
+
</dict>
|
|
3099
|
+
`;
|
|
3100
|
+
result = result.slice(0, closingDict) + envBlock + result.slice(closingDict);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return result;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/ios-companion/launcher.ts
|
|
3108
|
+
var PKG_ROOT = resolve(dirname4(fileURLToPath(import.meta.url)), "..");
|
|
3109
|
+
var PREBUILT_DIR = join5(PKG_ROOT, "ios-companion", "prebuilt");
|
|
3110
|
+
var SOURCE_DIR = join5(PKG_ROOT, "ios-companion");
|
|
3111
|
+
var POLL_INTERVAL_MS = 500;
|
|
3112
|
+
var PREBUILT_LAUNCH_TIMEOUT_MS = 3e4;
|
|
3113
|
+
var SOURCE_LAUNCH_TIMEOUT_MS = 12e4;
|
|
3114
|
+
function resolveLaunchStrategy(input) {
|
|
3115
|
+
if (input.localXcodeMajor === null && !input.prebuiltExists) {
|
|
3116
|
+
return { type: "error", reason: "Xcode is required for iOS simulator support" };
|
|
3117
|
+
}
|
|
3118
|
+
if (!input.prebuiltExists) {
|
|
3119
|
+
return { type: "source", sourceDir: input.sourceDir, reason: "prebuilt not found" };
|
|
3120
|
+
}
|
|
3121
|
+
if (input.buildInfoXcodeMajor === null) {
|
|
3122
|
+
return { type: "source", sourceDir: input.sourceDir, reason: "build-info.json missing or invalid" };
|
|
3123
|
+
}
|
|
3124
|
+
if (input.localXcodeMajor !== null && input.buildInfoXcodeMajor !== input.localXcodeMajor) {
|
|
3125
|
+
return {
|
|
3126
|
+
type: "source",
|
|
3127
|
+
sourceDir: input.sourceDir,
|
|
3128
|
+
reason: `Xcode major version mismatch: prebuilt=${input.buildInfoXcodeMajor}, local=${input.localXcodeMajor}`
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
return { type: "prebuilt", prebuiltDir: input.prebuiltDir };
|
|
3132
|
+
}
|
|
3133
|
+
function readBuildInfo(prebuiltDir) {
|
|
3134
|
+
try {
|
|
3135
|
+
const raw = readFileSync8(join5(prebuiltDir, "build-info.json"), "utf-8");
|
|
3136
|
+
return JSON.parse(raw);
|
|
3137
|
+
} catch {
|
|
3138
|
+
return null;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
async function getLocalXcodeMajor() {
|
|
3142
|
+
try {
|
|
3143
|
+
const { execSync } = await import("child_process");
|
|
3144
|
+
const output = execSync("xcodebuild -version", { encoding: "utf-8", timeout: 5e3 });
|
|
3145
|
+
const match = output.match(/Xcode\s+(\d+)/);
|
|
3146
|
+
return match ? parseInt(match[1], 10) : null;
|
|
3147
|
+
} catch {
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
var CompanionLauncher = class {
|
|
3152
|
+
processes = /* @__PURE__ */ new Map();
|
|
3153
|
+
clients = /* @__PURE__ */ new Map();
|
|
3154
|
+
portIndex = 0;
|
|
3155
|
+
basePort;
|
|
3156
|
+
constructor(basePort = 8300) {
|
|
3157
|
+
this.basePort = basePort;
|
|
3158
|
+
const cleanup = () => this.stop();
|
|
3159
|
+
process.on("exit", cleanup);
|
|
3160
|
+
process.on("SIGINT", cleanup);
|
|
3161
|
+
process.on("SIGTERM", cleanup);
|
|
3162
|
+
}
|
|
3163
|
+
async ensureRunning(deviceId, bundleId) {
|
|
3164
|
+
const existing = this.clients.get(deviceId);
|
|
3165
|
+
if (existing) {
|
|
3166
|
+
try {
|
|
3167
|
+
await existing.status();
|
|
3168
|
+
return existing;
|
|
3169
|
+
} catch {
|
|
3170
|
+
this.clients.delete(deviceId);
|
|
3171
|
+
this.killProcess(deviceId);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
const port = this.getPort(deviceId);
|
|
3175
|
+
const client = new CompanionClient(port);
|
|
3176
|
+
try {
|
|
3177
|
+
await client.status();
|
|
3178
|
+
this.clients.set(deviceId, client);
|
|
3179
|
+
return client;
|
|
3180
|
+
} catch {
|
|
3181
|
+
}
|
|
3182
|
+
const logger = getLogger();
|
|
3183
|
+
const buildInfo = readBuildInfo(PREBUILT_DIR);
|
|
3184
|
+
const localXcodeMajor = await getLocalXcodeMajor();
|
|
3185
|
+
const strategy = resolveLaunchStrategy({
|
|
3186
|
+
prebuiltDir: PREBUILT_DIR,
|
|
3187
|
+
prebuiltExists: existsSync6(join5(PREBUILT_DIR, "DriftxCompanionUITests.xctestrun")),
|
|
3188
|
+
buildInfoXcodeMajor: buildInfo?.xcodeMajor ?? null,
|
|
3189
|
+
localXcodeMajor,
|
|
3190
|
+
sourceDir: SOURCE_DIR,
|
|
3191
|
+
sourceExists: existsSync6(join5(SOURCE_DIR, "DriftxCompanion.xcodeproj"))
|
|
3192
|
+
});
|
|
3193
|
+
logger.debug(`Companion launch strategy: ${strategy.type}${strategy.reason ? ` (${strategy.reason})` : ""}`);
|
|
3194
|
+
if (strategy.type === "error") {
|
|
3195
|
+
throw new Error(strategy.reason ?? "Cannot launch iOS companion");
|
|
3196
|
+
}
|
|
3197
|
+
let launched = false;
|
|
3198
|
+
if (strategy.type === "prebuilt") {
|
|
3199
|
+
try {
|
|
3200
|
+
this.launchPrebuilt(deviceId, port);
|
|
3201
|
+
await this.waitForReadyWithEarlyExit(deviceId, client, PREBUILT_LAUNCH_TIMEOUT_MS);
|
|
3202
|
+
launched = true;
|
|
3203
|
+
} catch (err) {
|
|
3204
|
+
logger.debug(`Pre-built launch failed, falling back to source: ${err}`);
|
|
3205
|
+
this.killProcess(deviceId);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
if (!launched) {
|
|
3209
|
+
logger.debug(`Launching companion from source for device ${deviceId} on port ${port}`);
|
|
3210
|
+
this.launchSource(deviceId, port);
|
|
3211
|
+
await this.waitForReadyWithEarlyExit(deviceId, client, SOURCE_LAUNCH_TIMEOUT_MS);
|
|
3212
|
+
}
|
|
3213
|
+
if (bundleId) {
|
|
3214
|
+
logger.debug(`Configuring companion for bundle ${bundleId}`);
|
|
3215
|
+
await client.configure(bundleId);
|
|
3216
|
+
}
|
|
3217
|
+
this.clients.set(deviceId, client);
|
|
3218
|
+
return client;
|
|
3219
|
+
}
|
|
3220
|
+
stop(deviceId) {
|
|
3221
|
+
if (deviceId) {
|
|
3222
|
+
this.killProcess(deviceId);
|
|
3223
|
+
this.clients.delete(deviceId);
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
for (const id of this.processes.keys()) {
|
|
3227
|
+
this.killProcess(id);
|
|
3228
|
+
}
|
|
3229
|
+
this.clients.clear();
|
|
3230
|
+
}
|
|
3231
|
+
getPort(deviceId) {
|
|
3232
|
+
const existing = [...this.processes.keys()].indexOf(deviceId);
|
|
3233
|
+
if (existing >= 0) return this.basePort + existing;
|
|
3234
|
+
return this.basePort + this.portIndex++;
|
|
3235
|
+
}
|
|
3236
|
+
launchPrebuilt(deviceId, port) {
|
|
3237
|
+
const xctestrunPath = join5(PREBUILT_DIR, "DriftxCompanionUITests.xctestrun");
|
|
3238
|
+
const original = readFileSync8(xctestrunPath, "utf-8");
|
|
3239
|
+
const rewritten = rewriteXctestrun(original, PREBUILT_DIR, port);
|
|
3240
|
+
const tmpXctestrun = join5(tmpdir3(), `driftx-companion-${deviceId}-${port}.xctestrun`);
|
|
3241
|
+
writeFileSync4(tmpXctestrun, rewritten);
|
|
3242
|
+
const proc = spawn("xcodebuild", [
|
|
3243
|
+
"test-without-building",
|
|
3244
|
+
"-xctestrun",
|
|
3245
|
+
tmpXctestrun,
|
|
3246
|
+
"-destination",
|
|
3247
|
+
`platform=iOS Simulator,id=${deviceId}`,
|
|
3248
|
+
"-only-testing:DriftxCompanionUITests/DriftxCompanionUITests/testCompanionServer"
|
|
3249
|
+
], {
|
|
3250
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3251
|
+
});
|
|
3252
|
+
const logger = getLogger();
|
|
3253
|
+
proc.stdout?.on("data", (data) => {
|
|
3254
|
+
logger.debug(`[companion:${deviceId}] ${data.toString().trimEnd()}`);
|
|
3255
|
+
});
|
|
3256
|
+
proc.stderr?.on("data", (data) => {
|
|
3257
|
+
logger.debug(`[companion:${deviceId}] ${data.toString().trimEnd()}`);
|
|
3258
|
+
});
|
|
3259
|
+
proc.on("exit", (code) => {
|
|
3260
|
+
logger.debug(`Companion (prebuilt) for ${deviceId} exited with code ${code}`);
|
|
3261
|
+
this.processes.delete(deviceId);
|
|
3262
|
+
});
|
|
3263
|
+
this.processes.set(deviceId, proc);
|
|
3264
|
+
}
|
|
3265
|
+
launchSource(deviceId, port) {
|
|
3266
|
+
const env = { ...process.env, DRIFTX_PORT: String(port) };
|
|
3267
|
+
const proc = spawn("xcodebuild", [
|
|
3268
|
+
"test",
|
|
3269
|
+
"-project",
|
|
3270
|
+
resolve(SOURCE_DIR, "DriftxCompanion.xcodeproj"),
|
|
3271
|
+
"-scheme",
|
|
3272
|
+
"DriftxCompanionUITests",
|
|
3273
|
+
"-destination",
|
|
3274
|
+
`platform=iOS Simulator,id=${deviceId}`,
|
|
3275
|
+
"-only-testing:DriftxCompanionUITests/DriftxCompanionUITests/testCompanionServer"
|
|
3276
|
+
], {
|
|
3277
|
+
env,
|
|
3278
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3279
|
+
});
|
|
3280
|
+
const logger = getLogger();
|
|
3281
|
+
proc.stdout?.on("data", (data) => {
|
|
3282
|
+
logger.debug(`[companion:${deviceId}] ${data.toString().trimEnd()}`);
|
|
3283
|
+
});
|
|
3284
|
+
proc.stderr?.on("data", (data) => {
|
|
3285
|
+
logger.debug(`[companion:${deviceId}] ${data.toString().trimEnd()}`);
|
|
3286
|
+
});
|
|
3287
|
+
proc.on("exit", (code) => {
|
|
3288
|
+
logger.debug(`Companion (source) for ${deviceId} exited with code ${code}`);
|
|
3289
|
+
this.processes.delete(deviceId);
|
|
3290
|
+
});
|
|
3291
|
+
this.processes.set(deviceId, proc);
|
|
3292
|
+
}
|
|
3293
|
+
async waitForReadyWithEarlyExit(deviceId, client, timeoutMs) {
|
|
3294
|
+
const proc = this.processes.get(deviceId);
|
|
3295
|
+
let earlyExitCode = null;
|
|
3296
|
+
const exitListener = (code) => {
|
|
3297
|
+
earlyExitCode = code;
|
|
3298
|
+
};
|
|
3299
|
+
proc?.on("exit", exitListener);
|
|
3300
|
+
const deadline = Date.now() + timeoutMs;
|
|
3301
|
+
try {
|
|
3302
|
+
while (Date.now() < deadline) {
|
|
3303
|
+
if (earlyExitCode !== null) {
|
|
3304
|
+
throw new Error(`xcodebuild exited early with code ${earlyExitCode}`);
|
|
3305
|
+
}
|
|
3306
|
+
try {
|
|
3307
|
+
await client.status();
|
|
3308
|
+
return;
|
|
3309
|
+
} catch {
|
|
3310
|
+
await this.sleep(POLL_INTERVAL_MS);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
throw new Error(`Companion server did not become ready within ${timeoutMs}ms`);
|
|
3314
|
+
} finally {
|
|
3315
|
+
proc?.removeListener("exit", exitListener);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
killProcess(deviceId) {
|
|
3319
|
+
const proc = this.processes.get(deviceId);
|
|
3320
|
+
if (proc) {
|
|
3321
|
+
try {
|
|
3322
|
+
proc.kill("SIGTERM");
|
|
3323
|
+
} catch {
|
|
3324
|
+
}
|
|
3325
|
+
this.processes.delete(deviceId);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
sleep(ms) {
|
|
3329
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
|
|
3333
|
+
// src/update-notifier.ts
|
|
3334
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
3335
|
+
import { join as join6 } from "path";
|
|
3336
|
+
import { homedir } from "os";
|
|
3337
|
+
var CACHE_DIR = join6(homedir(), ".driftx");
|
|
3338
|
+
var CACHE_FILE = join6(CACHE_DIR, "update-check.json");
|
|
3339
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3340
|
+
function shouldCheck(cache) {
|
|
3341
|
+
if (!cache) return true;
|
|
3342
|
+
return Date.now() - cache.lastCheck > CHECK_INTERVAL_MS;
|
|
3343
|
+
}
|
|
3344
|
+
function formatUpdateMessage(current, latest) {
|
|
3345
|
+
if (compareVersions(current, latest) >= 0) return null;
|
|
3346
|
+
return ` Update available: ${current} \u2192 ${latest}
|
|
3347
|
+
Run: npm install -g driftx`;
|
|
3348
|
+
}
|
|
3349
|
+
function compareVersions(a, b) {
|
|
3350
|
+
const pa = a.split(".").map(Number);
|
|
3351
|
+
const pb = b.split(".").map(Number);
|
|
3352
|
+
for (let i = 0; i < 3; i++) {
|
|
3353
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
3354
|
+
if (diff !== 0) return diff;
|
|
3355
|
+
}
|
|
3356
|
+
return 0;
|
|
3357
|
+
}
|
|
3358
|
+
function readCache() {
|
|
3359
|
+
try {
|
|
3360
|
+
return JSON.parse(readFileSync9(CACHE_FILE, "utf-8"));
|
|
3361
|
+
} catch {
|
|
3362
|
+
return null;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function writeCache(cache) {
|
|
3366
|
+
try {
|
|
3367
|
+
mkdirSync4(CACHE_DIR, { recursive: true });
|
|
3368
|
+
writeFileSync5(CACHE_FILE, JSON.stringify(cache));
|
|
3369
|
+
} catch {
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
async function fetchLatestVersion() {
|
|
3373
|
+
try {
|
|
3374
|
+
const controller = new AbortController();
|
|
3375
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
3376
|
+
const res = await fetch("https://registry.npmjs.org/driftx/latest", { signal: controller.signal });
|
|
3377
|
+
clearTimeout(timer);
|
|
3378
|
+
if (!res.ok) return null;
|
|
3379
|
+
const data = await res.json();
|
|
3380
|
+
return data.version ?? null;
|
|
3381
|
+
} catch {
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
async function checkForUpdate(currentVersion) {
|
|
3386
|
+
const cache = readCache();
|
|
3387
|
+
if (!shouldCheck(cache)) {
|
|
3388
|
+
return cache ? formatUpdateMessage(currentVersion, cache.latestVersion) : null;
|
|
3389
|
+
}
|
|
3390
|
+
fetchLatestVersion().then((latest) => {
|
|
3391
|
+
if (latest) {
|
|
3392
|
+
writeCache({ lastCheck: Date.now(), latestVersion: latest });
|
|
3393
|
+
}
|
|
3394
|
+
}).catch(() => {
|
|
3395
|
+
});
|
|
3396
|
+
return cache ? formatUpdateMessage(currentVersion, cache.latestVersion) : null;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
2724
3399
|
// src/cli.ts
|
|
2725
3400
|
var require2 = createRequire(import.meta.url);
|
|
2726
3401
|
var pkg = require2("../package.json");
|
|
@@ -2732,8 +3407,27 @@ function getFormatterContext(opts) {
|
|
|
2732
3407
|
};
|
|
2733
3408
|
}
|
|
2734
3409
|
function createProgram() {
|
|
3410
|
+
let companionLauncher;
|
|
3411
|
+
let resolvedBundleId;
|
|
3412
|
+
function getCompanionLauncher(port) {
|
|
3413
|
+
if (!companionLauncher) {
|
|
3414
|
+
companionLauncher = new CompanionLauncher(port);
|
|
3415
|
+
}
|
|
3416
|
+
return companionLauncher;
|
|
3417
|
+
}
|
|
3418
|
+
async function getBundleId(program2, metroPort, deviceName) {
|
|
3419
|
+
const explicit = program2.opts().bundleId;
|
|
3420
|
+
if (explicit) return explicit;
|
|
3421
|
+
if (resolvedBundleId) return resolvedBundleId;
|
|
3422
|
+
const detected = await detectBundleId(metroPort, deviceName);
|
|
3423
|
+
if (detected) {
|
|
3424
|
+
resolvedBundleId = detected;
|
|
3425
|
+
getLogger().debug(`Auto-detected bundle ID: ${detected}`);
|
|
3426
|
+
}
|
|
3427
|
+
return resolvedBundleId;
|
|
3428
|
+
}
|
|
2735
3429
|
const program = new Command();
|
|
2736
|
-
program.name("driftx").description("Visual diff tool for React Native and Android development").version(pkg.version).option("--verbose", "enable debug logging").option("--quiet", "suppress all output except errors").option("--format <type>", "output format: terminal, markdown, json", "terminal").option("--copy", "copy output to clipboard");
|
|
3430
|
+
program.name("driftx").description("Visual diff tool for React Native and Android development").version(pkg.version).option("--verbose", "enable debug logging").option("--quiet", "suppress all output except errors").option("--format <type>", "output format: terminal, markdown, json", "terminal").option("--copy", "copy output to clipboard").option("--bundle-id <id>", "iOS app bundle identifier for companion");
|
|
2737
3431
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
2738
3432
|
const opts = actionCommand.optsWithGlobals();
|
|
2739
3433
|
const level = opts.verbose ? "debug" : opts.quiet ? "silent" : "info";
|
|
@@ -2752,13 +3446,13 @@ function createProgram() {
|
|
|
2752
3446
|
const files = readdirSync2(cwd);
|
|
2753
3447
|
let packageJson;
|
|
2754
3448
|
try {
|
|
2755
|
-
packageJson = JSON.parse(
|
|
3449
|
+
packageJson = JSON.parse(readFileSync10(join7(cwd, "package.json"), "utf-8"));
|
|
2756
3450
|
} catch {
|
|
2757
3451
|
}
|
|
2758
3452
|
const framework = detectFramework(files, packageJson);
|
|
2759
3453
|
const config = generateConfig(framework);
|
|
2760
|
-
const configPath =
|
|
2761
|
-
|
|
3454
|
+
const configPath = join7(cwd, ".driftxrc.json");
|
|
3455
|
+
writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
2762
3456
|
console.log(`Created ${configPath} (framework: ${framework})`);
|
|
2763
3457
|
});
|
|
2764
3458
|
program.command("devices").description("List connected devices and simulators").action(async function() {
|
|
@@ -2814,13 +3508,15 @@ function createProgram() {
|
|
|
2814
3508
|
} else {
|
|
2815
3509
|
device = await pickDevice(booted);
|
|
2816
3510
|
}
|
|
2817
|
-
const
|
|
3511
|
+
const launcher = device.platform === "ios" ? getCompanionLauncher(config.companionPort) : void 0;
|
|
3512
|
+
const inspector = new TreeInspector(shell, process.cwd(), launcher);
|
|
3513
|
+
const globalOpts = this.optsWithGlobals();
|
|
2818
3514
|
const result = await inspector.inspect(device, {
|
|
2819
3515
|
metroPort: config.metroPort,
|
|
2820
3516
|
devToolsPort: config.devToolsPort,
|
|
2821
|
-
timeoutMs: config.timeouts.treeInspectionMs
|
|
3517
|
+
timeoutMs: config.timeouts.treeInspectionMs,
|
|
3518
|
+
bundleId: await getBundleId(program, config.metroPort, device.name)
|
|
2822
3519
|
});
|
|
2823
|
-
const globalOpts = this.optsWithGlobals();
|
|
2824
3520
|
if (opts.json) globalOpts.format = "json";
|
|
2825
3521
|
const ctx = getFormatterContext(globalOpts);
|
|
2826
3522
|
await formatOutput(inspectFormatter, result, ctx);
|
|
@@ -2839,20 +3535,46 @@ function createProgram() {
|
|
|
2839
3535
|
} else {
|
|
2840
3536
|
device = await pickDevice(booted);
|
|
2841
3537
|
}
|
|
2842
|
-
const
|
|
3538
|
+
const launcher = device.platform === "ios" ? getCompanionLauncher(config.companionPort) : void 0;
|
|
3539
|
+
const companion = launcher ? await launcher.ensureRunning(device.id, await getBundleId(program, config.metroPort, device.name)) : void 0;
|
|
3540
|
+
const backend = createBackend(shell, device.platform, companion);
|
|
2843
3541
|
const executor = new GestureExecutor(backend);
|
|
2844
3542
|
let result;
|
|
2845
3543
|
if (opts.xy) {
|
|
2846
3544
|
const [x, y] = target.split(",").map(Number);
|
|
2847
3545
|
result = await executor.tapXY(device, x, y);
|
|
2848
3546
|
} else {
|
|
2849
|
-
const inspector = new TreeInspector(shell, process.cwd());
|
|
3547
|
+
const inspector = new TreeInspector(shell, process.cwd(), launcher);
|
|
2850
3548
|
const inspectResult = await inspector.inspect(device, {
|
|
2851
3549
|
metroPort: config.metroPort,
|
|
2852
3550
|
devToolsPort: config.devToolsPort,
|
|
2853
|
-
timeoutMs: config.timeouts.treeInspectionMs
|
|
3551
|
+
timeoutMs: config.timeouts.treeInspectionMs,
|
|
3552
|
+
bundleId: await getBundleId(program, config.metroPort, device.name)
|
|
2854
3553
|
});
|
|
2855
3554
|
result = await executor.tap(device, inspectResult.tree, target);
|
|
3555
|
+
if (!result.success && result.error?.includes("Target not found") && companion) {
|
|
3556
|
+
const rawNodes = await companion.hierarchy();
|
|
3557
|
+
const companionTree = parseCompanionHierarchy(rawNodes);
|
|
3558
|
+
result = await executor.tap(device, companionTree, target);
|
|
3559
|
+
}
|
|
3560
|
+
if (!result.success && result.error?.includes("Target not found") && companion) {
|
|
3561
|
+
const frame = await companion.find(target);
|
|
3562
|
+
if (frame) {
|
|
3563
|
+
const cx = Math.round(frame.x + frame.width / 2);
|
|
3564
|
+
const cy = Math.round(frame.y + frame.height / 2);
|
|
3565
|
+
result = await executor.tapXY(device, cx, cy);
|
|
3566
|
+
result.target = { x: cx, y: cy, resolvedFrom: `xcuitest-find:${target}` };
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
if (!result.success && result.error?.includes("Target not found")) {
|
|
3570
|
+
const bounds = await measureElementByText(config.metroPort, target, device.name);
|
|
3571
|
+
if (bounds) {
|
|
3572
|
+
const cx = Math.round(bounds.x + bounds.width / 2);
|
|
3573
|
+
const cy = Math.round(bounds.y + bounds.height / 2);
|
|
3574
|
+
result = await executor.tapXY(device, cx, cy);
|
|
3575
|
+
result.target = { x: cx, y: cy, resolvedFrom: `cdp-measure:${target}` };
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
2856
3578
|
}
|
|
2857
3579
|
console.log(JSON.stringify(result, null, 2));
|
|
2858
3580
|
});
|
|
@@ -2870,19 +3592,38 @@ function createProgram() {
|
|
|
2870
3592
|
} else {
|
|
2871
3593
|
device = await pickDevice(booted);
|
|
2872
3594
|
}
|
|
2873
|
-
const
|
|
3595
|
+
const launcher = device.platform === "ios" ? getCompanionLauncher(config.companionPort) : void 0;
|
|
3596
|
+
const companion = launcher ? await launcher.ensureRunning(device.id, await getBundleId(program, config.metroPort, device.name)) : void 0;
|
|
3597
|
+
const inspector = new TreeInspector(shell, process.cwd(), launcher);
|
|
2874
3598
|
const inspectResult = await inspector.inspect(device, {
|
|
2875
3599
|
metroPort: config.metroPort,
|
|
2876
3600
|
devToolsPort: config.devToolsPort,
|
|
2877
|
-
timeoutMs: config.timeouts.treeInspectionMs
|
|
3601
|
+
timeoutMs: config.timeouts.treeInspectionMs,
|
|
3602
|
+
bundleId: await getBundleId(program, config.metroPort, device.name)
|
|
2878
3603
|
});
|
|
2879
|
-
const backend = createBackend(shell, device.platform);
|
|
3604
|
+
const backend = createBackend(shell, device.platform, companion);
|
|
2880
3605
|
const executor = new GestureExecutor(backend);
|
|
2881
|
-
|
|
3606
|
+
let result = await executor.typeInto(device, inspectResult.tree, target, text);
|
|
3607
|
+
if (!result.success && result.error?.includes("Target not found") && companion) {
|
|
3608
|
+
const rawNodes = await companion.hierarchy();
|
|
3609
|
+
const companionTree = parseCompanionHierarchy(rawNodes);
|
|
3610
|
+
result = await executor.typeInto(device, companionTree, target, text);
|
|
3611
|
+
}
|
|
3612
|
+
if (!result.success && result.error?.includes("Target not found") && companion) {
|
|
3613
|
+
const frame = await companion.find(target);
|
|
3614
|
+
if (frame) {
|
|
3615
|
+
const cx = Math.round(frame.x + frame.width / 2);
|
|
3616
|
+
const cy = Math.round(frame.y + frame.height / 2);
|
|
3617
|
+
await backend.tap(device, { x: cx, y: cy });
|
|
3618
|
+
await backend.type(device, text);
|
|
3619
|
+
result = { success: true, action: "typeInto", target: { x: cx, y: cy, resolvedFrom: `xcuitest-find:${target}` }, durationMs: Date.now() };
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
2882
3622
|
console.log(JSON.stringify(result, null, 2));
|
|
2883
3623
|
});
|
|
2884
|
-
program.command("swipe <direction>").description("Swipe up, down, left, or right").option("-d, --device <id>", "device ID or name").action(async (direction, opts) => {
|
|
3624
|
+
program.command("swipe <direction>").description("Swipe up, down, left, or right").option("-d, --device <id>", "device ID or name").option("--distance <n>", "swipe distance in points", parseInt).action(async (direction, opts) => {
|
|
2885
3625
|
const shell = new RealShell();
|
|
3626
|
+
const config = await loadConfig();
|
|
2886
3627
|
const discovery = new DeviceDiscovery(shell);
|
|
2887
3628
|
const devices = await discovery.list();
|
|
2888
3629
|
const booted = devices.filter((d) => d.state === "booted");
|
|
@@ -2894,13 +3635,16 @@ function createProgram() {
|
|
|
2894
3635
|
} else {
|
|
2895
3636
|
device = await pickDevice(booted);
|
|
2896
3637
|
}
|
|
2897
|
-
const
|
|
3638
|
+
const launcher = device.platform === "ios" ? getCompanionLauncher(config.companionPort) : void 0;
|
|
3639
|
+
const companion = launcher ? await launcher.ensureRunning(device.id, await getBundleId(program, config.metroPort, device.name)) : void 0;
|
|
3640
|
+
const backend = createBackend(shell, device.platform, companion);
|
|
2898
3641
|
const executor = new GestureExecutor(backend);
|
|
2899
|
-
const result = await executor.swipe(device, direction);
|
|
3642
|
+
const result = await executor.swipe(device, direction, opts.distance);
|
|
2900
3643
|
console.log(JSON.stringify(result, null, 2));
|
|
2901
3644
|
});
|
|
2902
3645
|
program.command("go-back").description("Press the back button").option("-d, --device <id>", "device ID or name").action(async (opts) => {
|
|
2903
3646
|
const shell = new RealShell();
|
|
3647
|
+
const config = await loadConfig();
|
|
2904
3648
|
const discovery = new DeviceDiscovery(shell);
|
|
2905
3649
|
const devices = await discovery.list();
|
|
2906
3650
|
const booted = devices.filter((d) => d.state === "booted");
|
|
@@ -2912,13 +3656,16 @@ function createProgram() {
|
|
|
2912
3656
|
} else {
|
|
2913
3657
|
device = await pickDevice(booted);
|
|
2914
3658
|
}
|
|
2915
|
-
const
|
|
3659
|
+
const launcher = device.platform === "ios" ? getCompanionLauncher(config.companionPort) : void 0;
|
|
3660
|
+
const companion = launcher ? await launcher.ensureRunning(device.id, await getBundleId(program, config.metroPort, device.name)) : void 0;
|
|
3661
|
+
const backend = createBackend(shell, device.platform, companion);
|
|
2916
3662
|
const executor = new GestureExecutor(backend);
|
|
2917
3663
|
const result = await executor.goBack(device);
|
|
2918
3664
|
console.log(JSON.stringify(result, null, 2));
|
|
2919
3665
|
});
|
|
2920
3666
|
program.command("open-url <url>").description("Open a deep link or URL on the device").option("-d, --device <id>", "device ID or name").action(async (url, opts) => {
|
|
2921
3667
|
const shell = new RealShell();
|
|
3668
|
+
const config = await loadConfig();
|
|
2922
3669
|
const discovery = new DeviceDiscovery(shell);
|
|
2923
3670
|
const devices = await discovery.list();
|
|
2924
3671
|
const booted = devices.filter((d) => d.state === "booted");
|
|
@@ -2930,25 +3677,27 @@ function createProgram() {
|
|
|
2930
3677
|
} else {
|
|
2931
3678
|
device = await pickDevice(booted);
|
|
2932
3679
|
}
|
|
2933
|
-
const
|
|
3680
|
+
const launcher = device.platform === "ios" ? getCompanionLauncher(config.companionPort) : void 0;
|
|
3681
|
+
const companion = launcher ? await launcher.ensureRunning(device.id, await getBundleId(program, config.metroPort, device.name)) : void 0;
|
|
3682
|
+
const backend = createBackend(shell, device.platform, companion);
|
|
2934
3683
|
const executor = new GestureExecutor(backend);
|
|
2935
3684
|
const result = await executor.openUrl(device, url);
|
|
2936
3685
|
console.log(JSON.stringify(result, null, 2));
|
|
2937
3686
|
});
|
|
2938
3687
|
program.command("setup-claude").description("Register driftx as a Claude Code plugin").action(() => {
|
|
2939
|
-
const claudeDir =
|
|
2940
|
-
const pluginsDir =
|
|
2941
|
-
const driftxPluginDir =
|
|
2942
|
-
const registryPath =
|
|
2943
|
-
const packageRoot =
|
|
2944
|
-
const skillSource =
|
|
2945
|
-
if (!
|
|
3688
|
+
const claudeDir = join7(homedir2(), ".claude");
|
|
3689
|
+
const pluginsDir = join7(claudeDir, "plugins");
|
|
3690
|
+
const driftxPluginDir = join7(pluginsDir, "driftx");
|
|
3691
|
+
const registryPath = join7(pluginsDir, "installed_plugins.json");
|
|
3692
|
+
const packageRoot = resolve2(dirname5(fileURLToPath2(import.meta.url)), "..");
|
|
3693
|
+
const skillSource = join7(packageRoot, "driftx-plugin");
|
|
3694
|
+
if (!existsSync7(skillSource)) {
|
|
2946
3695
|
console.error(`driftx-plugin directory not found at ${skillSource}`);
|
|
2947
3696
|
process.exitCode = 1;
|
|
2948
3697
|
return;
|
|
2949
3698
|
}
|
|
2950
|
-
|
|
2951
|
-
if (
|
|
3699
|
+
mkdirSync5(pluginsDir, { recursive: true });
|
|
3700
|
+
if (existsSync7(driftxPluginDir)) {
|
|
2952
3701
|
try {
|
|
2953
3702
|
unlinkSync3(driftxPluginDir);
|
|
2954
3703
|
} catch {
|
|
@@ -2960,7 +3709,7 @@ function createProgram() {
|
|
|
2960
3709
|
symlinkSync(skillSource, driftxPluginDir);
|
|
2961
3710
|
let registry = { version: 2, plugins: {} };
|
|
2962
3711
|
try {
|
|
2963
|
-
registry = JSON.parse(
|
|
3712
|
+
registry = JSON.parse(readFileSync10(registryPath, "utf-8"));
|
|
2964
3713
|
} catch {
|
|
2965
3714
|
}
|
|
2966
3715
|
registry.plugins["driftx@local"] = [{
|
|
@@ -2970,7 +3719,7 @@ function createProgram() {
|
|
|
2970
3719
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2971
3720
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2972
3721
|
}];
|
|
2973
|
-
|
|
3722
|
+
writeFileSync6(registryPath, JSON.stringify(registry, null, 2));
|
|
2974
3723
|
console.log("driftx registered as Claude Code plugin.");
|
|
2975
3724
|
console.log(` Symlink: ${driftxPluginDir} -> ${skillSource}`);
|
|
2976
3725
|
console.log(` Registry: ${registryPath}`);
|
|
@@ -2980,6 +3729,10 @@ function createProgram() {
|
|
|
2980
3729
|
}
|
|
2981
3730
|
function run(argv) {
|
|
2982
3731
|
const program = createProgram();
|
|
3732
|
+
checkForUpdate(pkg.version).then((msg) => {
|
|
3733
|
+
if (msg) process.stderr.write(msg + "\n");
|
|
3734
|
+
}).catch(() => {
|
|
3735
|
+
});
|
|
2983
3736
|
program.parseAsync(argv);
|
|
2984
3737
|
}
|
|
2985
3738
|
|