driftx 0.1.0 → 0.1.2
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 +842 -84
- package/dist/bin.js.map +1 -1
- package/driftx-plugin/skills/driftx/SKILL.md +514 -0
- 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 +19 -4
- package/driftx-plugin/skills/driftx.md +0 -299
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"));
|
|
@@ -667,6 +670,13 @@ async function withRetry(fn, policy, signal) {
|
|
|
667
670
|
import { select } from "@inquirer/prompts";
|
|
668
671
|
async function pickDevice(booted) {
|
|
669
672
|
if (booted.length === 1) return booted[0];
|
|
673
|
+
if (!process.stdout.isTTY) {
|
|
674
|
+
const list = booted.map((d) => ` - "${d.name}" (${d.platform}, ${d.id})`).join("\n");
|
|
675
|
+
throw new Error(
|
|
676
|
+
`Multiple booted devices found. Use --device to specify one:
|
|
677
|
+
${list}`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
670
680
|
const selected = await select({
|
|
671
681
|
message: "Select a device",
|
|
672
682
|
choices: booted.map((d) => ({
|
|
@@ -901,8 +911,133 @@ var FIBER_WALK_SCRIPT = `(function() {
|
|
|
901
911
|
}
|
|
902
912
|
}
|
|
903
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
|
+
}
|
|
904
1039
|
async function discoverTargets(metroPort) {
|
|
905
|
-
return new Promise((
|
|
1040
|
+
return new Promise((resolve3, reject) => {
|
|
906
1041
|
const req = http.get(`http://localhost:${metroPort}/json/list`, { timeout: 2e3 }, (res) => {
|
|
907
1042
|
let data = "";
|
|
908
1043
|
res.on("data", (chunk) => {
|
|
@@ -910,23 +1045,21 @@ async function discoverTargets(metroPort) {
|
|
|
910
1045
|
});
|
|
911
1046
|
res.on("end", () => {
|
|
912
1047
|
try {
|
|
913
|
-
|
|
1048
|
+
resolve3(JSON.parse(data));
|
|
914
1049
|
} catch {
|
|
915
|
-
|
|
1050
|
+
resolve3([]);
|
|
916
1051
|
}
|
|
917
1052
|
});
|
|
918
1053
|
});
|
|
919
|
-
req.on("error", () =>
|
|
1054
|
+
req.on("error", () => resolve3([]));
|
|
920
1055
|
req.on("timeout", () => {
|
|
921
1056
|
req.destroy();
|
|
922
|
-
|
|
1057
|
+
resolve3([]);
|
|
923
1058
|
});
|
|
924
1059
|
});
|
|
925
1060
|
}
|
|
926
1061
|
function findRuntimeTarget(targets, deviceName) {
|
|
927
|
-
const rnTargets = targets.filter(
|
|
928
|
-
(t) => t.reactNative && t.description.includes("React Native")
|
|
929
|
-
);
|
|
1062
|
+
const rnTargets = targets.filter((t) => !!t.reactNative);
|
|
930
1063
|
if (deviceName) {
|
|
931
1064
|
return rnTargets.find(
|
|
932
1065
|
(t) => t.deviceName?.toLowerCase().includes(deviceName.toLowerCase())
|
|
@@ -948,16 +1081,16 @@ var CdpClient = class {
|
|
|
948
1081
|
return [];
|
|
949
1082
|
}
|
|
950
1083
|
logger.debug(`Found CDP target: ${target.title} (${target.description})`);
|
|
951
|
-
return new Promise((
|
|
1084
|
+
return new Promise((resolve3, reject) => {
|
|
952
1085
|
const timer = setTimeout(() => {
|
|
953
1086
|
this.cleanup();
|
|
954
|
-
|
|
1087
|
+
resolve3([]);
|
|
955
1088
|
}, timeoutMs);
|
|
956
1089
|
try {
|
|
957
1090
|
this.ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
958
1091
|
} catch {
|
|
959
1092
|
clearTimeout(timer);
|
|
960
|
-
|
|
1093
|
+
resolve3([]);
|
|
961
1094
|
return;
|
|
962
1095
|
}
|
|
963
1096
|
this.ws.on("open", async () => {
|
|
@@ -965,10 +1098,10 @@ var CdpClient = class {
|
|
|
965
1098
|
try {
|
|
966
1099
|
const result = await this.evaluate(FIBER_WALK_SCRIPT);
|
|
967
1100
|
clearTimeout(timer);
|
|
968
|
-
|
|
1101
|
+
resolve3(this.parseResult(result));
|
|
969
1102
|
} catch {
|
|
970
1103
|
clearTimeout(timer);
|
|
971
|
-
|
|
1104
|
+
resolve3([]);
|
|
972
1105
|
}
|
|
973
1106
|
});
|
|
974
1107
|
this.ws.on("message", (data) => {
|
|
@@ -986,7 +1119,7 @@ var CdpClient = class {
|
|
|
986
1119
|
});
|
|
987
1120
|
this.ws.on("error", () => {
|
|
988
1121
|
clearTimeout(timer);
|
|
989
|
-
|
|
1122
|
+
resolve3([]);
|
|
990
1123
|
});
|
|
991
1124
|
this.ws.on("close", () => {
|
|
992
1125
|
this.connected = false;
|
|
@@ -995,8 +1128,8 @@ var CdpClient = class {
|
|
|
995
1128
|
}
|
|
996
1129
|
evaluate(expression) {
|
|
997
1130
|
const id = ++this.msgId;
|
|
998
|
-
return new Promise((
|
|
999
|
-
this.pending.set(id, { resolve:
|
|
1131
|
+
return new Promise((resolve3, reject) => {
|
|
1132
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
1000
1133
|
this.ws.send(JSON.stringify({
|
|
1001
1134
|
id,
|
|
1002
1135
|
method: "Runtime.evaluate",
|
|
@@ -1163,13 +1296,56 @@ var StrategyCache = class {
|
|
|
1163
1296
|
}
|
|
1164
1297
|
};
|
|
1165
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
|
+
|
|
1166
1340
|
// src/inspect/tree-inspector.ts
|
|
1167
1341
|
var TreeInspector = class {
|
|
1168
1342
|
shell;
|
|
1169
1343
|
fileCache;
|
|
1170
|
-
|
|
1344
|
+
companionLauncher;
|
|
1345
|
+
constructor(shell, projectRoot, companionLauncher) {
|
|
1171
1346
|
this.shell = shell;
|
|
1172
1347
|
this.fileCache = projectRoot ? new StrategyCache(projectRoot) : null;
|
|
1348
|
+
this.companionLauncher = companionLauncher ?? null;
|
|
1173
1349
|
}
|
|
1174
1350
|
invalidateCache(deviceId) {
|
|
1175
1351
|
if (!this.fileCache) return;
|
|
@@ -1195,6 +1371,9 @@ var TreeInspector = class {
|
|
|
1195
1371
|
if (device.platform === "android") {
|
|
1196
1372
|
return { method: "uiautomator", reason: "Android native inspection" };
|
|
1197
1373
|
}
|
|
1374
|
+
if (this.companionLauncher) {
|
|
1375
|
+
return { method: "xcuitest", reason: "iOS native inspection via XCUITest companion" };
|
|
1376
|
+
}
|
|
1198
1377
|
return { method: "idb", reason: "iOS native inspection via idb" };
|
|
1199
1378
|
}
|
|
1200
1379
|
async inspect(device, options) {
|
|
@@ -1252,13 +1431,30 @@ var TreeInspector = class {
|
|
|
1252
1431
|
logger.debug(`UIAutomator failed: ${err instanceof Error ? err.message : err}`);
|
|
1253
1432
|
}
|
|
1254
1433
|
}
|
|
1255
|
-
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") {
|
|
1256
1452
|
try {
|
|
1257
1453
|
const tree = await dumpIosAccessibility(this.shell, device.id, options.timeoutMs);
|
|
1258
1454
|
logger.debug(`idb: got ${tree.length} root nodes for ${device.name}`);
|
|
1259
1455
|
return {
|
|
1260
1456
|
...base,
|
|
1261
|
-
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 },
|
|
1262
1458
|
tree,
|
|
1263
1459
|
capabilities: { tree: "basic", sourceMapping: "none", styles: "none", protocol: "idb" },
|
|
1264
1460
|
hints
|
|
@@ -2318,12 +2514,12 @@ async function copyToClipboard(text) {
|
|
|
2318
2514
|
logger.debug(`Clipboard not supported on ${process.platform}`);
|
|
2319
2515
|
return;
|
|
2320
2516
|
}
|
|
2321
|
-
return new Promise((
|
|
2517
|
+
return new Promise((resolve3) => {
|
|
2322
2518
|
const proc = exec(cmd, (err) => {
|
|
2323
2519
|
if (err) {
|
|
2324
2520
|
logger.debug(`Clipboard copy failed: ${err.message}`);
|
|
2325
2521
|
}
|
|
2326
|
-
|
|
2522
|
+
resolve3();
|
|
2327
2523
|
});
|
|
2328
2524
|
proc.stdin?.write(text);
|
|
2329
2525
|
proc.stdin?.end();
|
|
@@ -2563,35 +2759,35 @@ var AndroidBackend = class {
|
|
|
2563
2759
|
|
|
2564
2760
|
// src/interact/ios.ts
|
|
2565
2761
|
var IosBackend = class {
|
|
2566
|
-
constructor(shell) {
|
|
2762
|
+
constructor(shell, companion) {
|
|
2567
2763
|
this.shell = shell;
|
|
2764
|
+
this.companion = companion;
|
|
2568
2765
|
}
|
|
2569
|
-
async tap(
|
|
2570
|
-
await this.
|
|
2766
|
+
async tap(_device, point) {
|
|
2767
|
+
await this.companion.tap(point.x, point.y);
|
|
2571
2768
|
}
|
|
2572
|
-
async longPress(
|
|
2573
|
-
await this.
|
|
2769
|
+
async longPress(_device, point, durationMs) {
|
|
2770
|
+
await this.companion.longPress(point.x, point.y, durationMs);
|
|
2574
2771
|
}
|
|
2575
|
-
async swipe(
|
|
2576
|
-
await this.
|
|
2772
|
+
async swipe(_device, from, to, durationMs) {
|
|
2773
|
+
await this.companion.swipe(from.x, from.y, to.x, to.y, durationMs);
|
|
2577
2774
|
}
|
|
2578
|
-
async type(
|
|
2579
|
-
await this.
|
|
2775
|
+
async type(_device, text) {
|
|
2776
|
+
await this.companion.type(text);
|
|
2580
2777
|
}
|
|
2581
|
-
async keyEvent(
|
|
2582
|
-
await this.
|
|
2778
|
+
async keyEvent(_device, key) {
|
|
2779
|
+
await this.companion.keyEvent(key);
|
|
2583
2780
|
}
|
|
2584
2781
|
async openUrl(device, url) {
|
|
2585
2782
|
await this.shell.exec("xcrun", ["simctl", "openurl", device.id, url]);
|
|
2586
2783
|
}
|
|
2587
|
-
async simctlIo(device, args) {
|
|
2588
|
-
await this.shell.exec("xcrun", ["simctl", "io", device.id, ...args]);
|
|
2589
|
-
}
|
|
2590
2784
|
};
|
|
2591
2785
|
|
|
2592
2786
|
// src/interact/backend.ts
|
|
2593
|
-
function createBackend(shell, platform) {
|
|
2594
|
-
|
|
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);
|
|
2595
2791
|
}
|
|
2596
2792
|
|
|
2597
2793
|
// src/interact/resolver.ts
|
|
@@ -2603,6 +2799,17 @@ function resolveTarget(tree, query) {
|
|
|
2603
2799
|
if (byName) return centerOf(byName, `name:${query}`);
|
|
2604
2800
|
const byText = nodes.find((n) => n.text === query && hasSize(n));
|
|
2605
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}`);
|
|
2606
2813
|
return null;
|
|
2607
2814
|
}
|
|
2608
2815
|
function flattenTree(nodes) {
|
|
@@ -2618,6 +2825,13 @@ function flattenTree(nodes) {
|
|
|
2618
2825
|
function hasSize(node) {
|
|
2619
2826
|
return node.bounds.width > 0 && node.bounds.height > 0;
|
|
2620
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
|
+
}
|
|
2621
2835
|
function centerOf(node, resolvedFrom) {
|
|
2622
2836
|
return {
|
|
2623
2837
|
x: Math.round(node.bounds.x + node.bounds.width / 2),
|
|
@@ -2667,13 +2881,15 @@ var GestureExecutor = class {
|
|
|
2667
2881
|
return { success: false, action: "longPress", durationMs: Date.now() - start, error: String(e) };
|
|
2668
2882
|
}
|
|
2669
2883
|
}
|
|
2670
|
-
async swipe(device, direction, distance
|
|
2884
|
+
async swipe(device, direction, distance, durationMs = 300) {
|
|
2671
2885
|
const start = Date.now();
|
|
2672
2886
|
try {
|
|
2673
|
-
const
|
|
2674
|
-
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);
|
|
2675
2891
|
const from = { x: cx, y: cy };
|
|
2676
|
-
const half = Math.round(
|
|
2892
|
+
const half = Math.round(actualDistance / 2);
|
|
2677
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 };
|
|
2678
2894
|
await this.backend.swipe(device, from, to, durationMs);
|
|
2679
2895
|
return { success: true, action: "swipe", durationMs: Date.now() - start };
|
|
@@ -2716,6 +2932,470 @@ var GestureExecutor = class {
|
|
|
2716
2932
|
}
|
|
2717
2933
|
};
|
|
2718
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
|
+
|
|
2719
3399
|
// src/cli.ts
|
|
2720
3400
|
var require2 = createRequire(import.meta.url);
|
|
2721
3401
|
var pkg = require2("../package.json");
|
|
@@ -2727,8 +3407,27 @@ function getFormatterContext(opts) {
|
|
|
2727
3407
|
};
|
|
2728
3408
|
}
|
|
2729
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
|
+
}
|
|
2730
3429
|
const program = new Command();
|
|
2731
|
-
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");
|
|
2732
3431
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
2733
3432
|
const opts = actionCommand.optsWithGlobals();
|
|
2734
3433
|
const level = opts.verbose ? "debug" : opts.quiet ? "silent" : "info";
|
|
@@ -2747,13 +3446,13 @@ function createProgram() {
|
|
|
2747
3446
|
const files = readdirSync2(cwd);
|
|
2748
3447
|
let packageJson;
|
|
2749
3448
|
try {
|
|
2750
|
-
packageJson = JSON.parse(
|
|
3449
|
+
packageJson = JSON.parse(readFileSync10(join7(cwd, "package.json"), "utf-8"));
|
|
2751
3450
|
} catch {
|
|
2752
3451
|
}
|
|
2753
3452
|
const framework = detectFramework(files, packageJson);
|
|
2754
3453
|
const config = generateConfig(framework);
|
|
2755
|
-
const configPath =
|
|
2756
|
-
|
|
3454
|
+
const configPath = join7(cwd, ".driftxrc.json");
|
|
3455
|
+
writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
2757
3456
|
console.log(`Created ${configPath} (framework: ${framework})`);
|
|
2758
3457
|
});
|
|
2759
3458
|
program.command("devices").description("List connected devices and simulators").action(async function() {
|
|
@@ -2809,13 +3508,15 @@ function createProgram() {
|
|
|
2809
3508
|
} else {
|
|
2810
3509
|
device = await pickDevice(booted);
|
|
2811
3510
|
}
|
|
2812
|
-
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();
|
|
2813
3514
|
const result = await inspector.inspect(device, {
|
|
2814
3515
|
metroPort: config.metroPort,
|
|
2815
3516
|
devToolsPort: config.devToolsPort,
|
|
2816
|
-
timeoutMs: config.timeouts.treeInspectionMs
|
|
3517
|
+
timeoutMs: config.timeouts.treeInspectionMs,
|
|
3518
|
+
bundleId: await getBundleId(program, config.metroPort, device.name)
|
|
2817
3519
|
});
|
|
2818
|
-
const globalOpts = this.optsWithGlobals();
|
|
2819
3520
|
if (opts.json) globalOpts.format = "json";
|
|
2820
3521
|
const ctx = getFormatterContext(globalOpts);
|
|
2821
3522
|
await formatOutput(inspectFormatter, result, ctx);
|
|
@@ -2834,20 +3535,46 @@ function createProgram() {
|
|
|
2834
3535
|
} else {
|
|
2835
3536
|
device = await pickDevice(booted);
|
|
2836
3537
|
}
|
|
2837
|
-
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);
|
|
2838
3541
|
const executor = new GestureExecutor(backend);
|
|
2839
3542
|
let result;
|
|
2840
3543
|
if (opts.xy) {
|
|
2841
3544
|
const [x, y] = target.split(",").map(Number);
|
|
2842
3545
|
result = await executor.tapXY(device, x, y);
|
|
2843
3546
|
} else {
|
|
2844
|
-
const inspector = new TreeInspector(shell, process.cwd());
|
|
3547
|
+
const inspector = new TreeInspector(shell, process.cwd(), launcher);
|
|
2845
3548
|
const inspectResult = await inspector.inspect(device, {
|
|
2846
3549
|
metroPort: config.metroPort,
|
|
2847
3550
|
devToolsPort: config.devToolsPort,
|
|
2848
|
-
timeoutMs: config.timeouts.treeInspectionMs
|
|
3551
|
+
timeoutMs: config.timeouts.treeInspectionMs,
|
|
3552
|
+
bundleId: await getBundleId(program, config.metroPort, device.name)
|
|
2849
3553
|
});
|
|
2850
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
|
+
}
|
|
2851
3578
|
}
|
|
2852
3579
|
console.log(JSON.stringify(result, null, 2));
|
|
2853
3580
|
});
|
|
@@ -2865,19 +3592,38 @@ function createProgram() {
|
|
|
2865
3592
|
} else {
|
|
2866
3593
|
device = await pickDevice(booted);
|
|
2867
3594
|
}
|
|
2868
|
-
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);
|
|
2869
3598
|
const inspectResult = await inspector.inspect(device, {
|
|
2870
3599
|
metroPort: config.metroPort,
|
|
2871
3600
|
devToolsPort: config.devToolsPort,
|
|
2872
|
-
timeoutMs: config.timeouts.treeInspectionMs
|
|
3601
|
+
timeoutMs: config.timeouts.treeInspectionMs,
|
|
3602
|
+
bundleId: await getBundleId(program, config.metroPort, device.name)
|
|
2873
3603
|
});
|
|
2874
|
-
const backend = createBackend(shell, device.platform);
|
|
3604
|
+
const backend = createBackend(shell, device.platform, companion);
|
|
2875
3605
|
const executor = new GestureExecutor(backend);
|
|
2876
|
-
|
|
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
|
+
}
|
|
2877
3622
|
console.log(JSON.stringify(result, null, 2));
|
|
2878
3623
|
});
|
|
2879
|
-
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) => {
|
|
2880
3625
|
const shell = new RealShell();
|
|
3626
|
+
const config = await loadConfig();
|
|
2881
3627
|
const discovery = new DeviceDiscovery(shell);
|
|
2882
3628
|
const devices = await discovery.list();
|
|
2883
3629
|
const booted = devices.filter((d) => d.state === "booted");
|
|
@@ -2889,13 +3635,16 @@ function createProgram() {
|
|
|
2889
3635
|
} else {
|
|
2890
3636
|
device = await pickDevice(booted);
|
|
2891
3637
|
}
|
|
2892
|
-
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);
|
|
2893
3641
|
const executor = new GestureExecutor(backend);
|
|
2894
|
-
const result = await executor.swipe(device, direction);
|
|
3642
|
+
const result = await executor.swipe(device, direction, opts.distance);
|
|
2895
3643
|
console.log(JSON.stringify(result, null, 2));
|
|
2896
3644
|
});
|
|
2897
3645
|
program.command("go-back").description("Press the back button").option("-d, --device <id>", "device ID or name").action(async (opts) => {
|
|
2898
3646
|
const shell = new RealShell();
|
|
3647
|
+
const config = await loadConfig();
|
|
2899
3648
|
const discovery = new DeviceDiscovery(shell);
|
|
2900
3649
|
const devices = await discovery.list();
|
|
2901
3650
|
const booted = devices.filter((d) => d.state === "booted");
|
|
@@ -2907,13 +3656,16 @@ function createProgram() {
|
|
|
2907
3656
|
} else {
|
|
2908
3657
|
device = await pickDevice(booted);
|
|
2909
3658
|
}
|
|
2910
|
-
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);
|
|
2911
3662
|
const executor = new GestureExecutor(backend);
|
|
2912
3663
|
const result = await executor.goBack(device);
|
|
2913
3664
|
console.log(JSON.stringify(result, null, 2));
|
|
2914
3665
|
});
|
|
2915
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) => {
|
|
2916
3667
|
const shell = new RealShell();
|
|
3668
|
+
const config = await loadConfig();
|
|
2917
3669
|
const discovery = new DeviceDiscovery(shell);
|
|
2918
3670
|
const devices = await discovery.list();
|
|
2919
3671
|
const booted = devices.filter((d) => d.state === "booted");
|
|
@@ -2925,25 +3677,27 @@ function createProgram() {
|
|
|
2925
3677
|
} else {
|
|
2926
3678
|
device = await pickDevice(booted);
|
|
2927
3679
|
}
|
|
2928
|
-
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);
|
|
2929
3683
|
const executor = new GestureExecutor(backend);
|
|
2930
3684
|
const result = await executor.openUrl(device, url);
|
|
2931
3685
|
console.log(JSON.stringify(result, null, 2));
|
|
2932
3686
|
});
|
|
2933
3687
|
program.command("setup-claude").description("Register driftx as a Claude Code plugin").action(() => {
|
|
2934
|
-
const claudeDir =
|
|
2935
|
-
const pluginsDir =
|
|
2936
|
-
const driftxPluginDir =
|
|
2937
|
-
const registryPath =
|
|
2938
|
-
const packageRoot =
|
|
2939
|
-
const skillSource =
|
|
2940
|
-
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)) {
|
|
2941
3695
|
console.error(`driftx-plugin directory not found at ${skillSource}`);
|
|
2942
3696
|
process.exitCode = 1;
|
|
2943
3697
|
return;
|
|
2944
3698
|
}
|
|
2945
|
-
|
|
2946
|
-
if (
|
|
3699
|
+
mkdirSync5(pluginsDir, { recursive: true });
|
|
3700
|
+
if (existsSync7(driftxPluginDir)) {
|
|
2947
3701
|
try {
|
|
2948
3702
|
unlinkSync3(driftxPluginDir);
|
|
2949
3703
|
} catch {
|
|
@@ -2955,7 +3709,7 @@ function createProgram() {
|
|
|
2955
3709
|
symlinkSync(skillSource, driftxPluginDir);
|
|
2956
3710
|
let registry = { version: 2, plugins: {} };
|
|
2957
3711
|
try {
|
|
2958
|
-
registry = JSON.parse(
|
|
3712
|
+
registry = JSON.parse(readFileSync10(registryPath, "utf-8"));
|
|
2959
3713
|
} catch {
|
|
2960
3714
|
}
|
|
2961
3715
|
registry.plugins["driftx@local"] = [{
|
|
@@ -2965,7 +3719,7 @@ function createProgram() {
|
|
|
2965
3719
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2966
3720
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2967
3721
|
}];
|
|
2968
|
-
|
|
3722
|
+
writeFileSync6(registryPath, JSON.stringify(registry, null, 2));
|
|
2969
3723
|
console.log("driftx registered as Claude Code plugin.");
|
|
2970
3724
|
console.log(` Symlink: ${driftxPluginDir} -> ${skillSource}`);
|
|
2971
3725
|
console.log(` Registry: ${registryPath}`);
|
|
@@ -2975,6 +3729,10 @@ function createProgram() {
|
|
|
2975
3729
|
}
|
|
2976
3730
|
function run(argv) {
|
|
2977
3731
|
const program = createProgram();
|
|
3732
|
+
checkForUpdate(pkg.version).then((msg) => {
|
|
3733
|
+
if (msg) process.stderr.write(msg + "\n");
|
|
3734
|
+
}).catch(() => {
|
|
3735
|
+
});
|
|
2978
3736
|
program.parseAsync(argv);
|
|
2979
3737
|
}
|
|
2980
3738
|
|