@houwert/conductor 0.24.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/input-server.js +28 -0
- package/dist/commands/launch-app.js +30 -3
- package/dist/commands/native-rn.js +110 -0
- package/dist/commands/native.js +320 -0
- package/dist/daemon/client.js +5 -0
- package/dist/daemon/input-backends.js +203 -0
- package/dist/daemon/input-protocol.js +40 -0
- package/dist/daemon/input-router.js +110 -0
- package/dist/daemon/input-server.js +124 -0
- package/dist/daemon/server.js +84 -0
- package/dist/drivers/bootstrap.js +49 -0
- package/dist/drivers/eval-compiler.js +131 -0
- package/dist/drivers/ios-hid.js +95 -0
- package/dist/drivers/ios-inproc.js +198 -0
- package/dist/drivers/ios.js +39 -8
- package/dist/drivers/metro-scripts.js +174 -0
- package/dist/index.js +141 -0
- package/dist/runner.js +33 -0
- package/package.json +1 -1
- package/skills/conductor-device-interact/SKILL.md +13 -0
- package/skills/conductor-device-setup/SKILL.md +1 -1
- package/skills/conductor-inspect/SKILL.md +43 -0
- package/skills/conductor-metro-debugger/SKILL.md +10 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
4
|
+
exports.inputServer = inputServer;
|
|
5
|
+
exports.HELP = ` input-server Start (if needed) and print the streaming-input WebSocket for the device`;
|
|
6
|
+
const runner_js_1 = require("../runner.js");
|
|
7
|
+
const output_js_1 = require("../output.js");
|
|
8
|
+
/**
|
|
9
|
+
* Ensure the device daemon + streaming-input socket are up and print the
|
|
10
|
+
* loopback WebSocket URL the host IDE connects to for pointer/key/button
|
|
11
|
+
* streaming. See docs/device-input-migration.md.
|
|
12
|
+
*/
|
|
13
|
+
async function inputServer(opts = {}, sessionName = 'default') {
|
|
14
|
+
try {
|
|
15
|
+
const info = await (0, runner_js_1.inputServerInfo)(sessionName);
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
(0, output_js_1.printData)(info, opts);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(`input server [${info.device}] (${info.platform}): ${info.url}`);
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
(0, output_js_1.printError)(`input-server — ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.HELP = void 0;
|
|
4
7
|
exports.launchApp = launchApp;
|
|
@@ -12,11 +15,16 @@ exports.HELP = ` launch-app <appId> Launch app (saves to sessi
|
|
|
12
15
|
the user out of every app on the simulator. Cannot be undone
|
|
13
16
|
without re-entering credentials.
|
|
14
17
|
--no-stop-app Do not stop the app before launching (resume instead of restart)
|
|
15
|
-
--argument key=value Set launch argument (repeatable)
|
|
18
|
+
--argument key=value Set launch argument (repeatable)
|
|
19
|
+
--inject iOS simulator only: inject the in-process control library
|
|
20
|
+
(DYLD_INSERT_LIBRARIES) so native-* inspection commands work`;
|
|
16
21
|
const runner_js_1 = require("../runner.js");
|
|
17
22
|
const session_js_1 = require("../session.js");
|
|
18
23
|
const output_js_1 = require("../output.js");
|
|
19
24
|
const ios_js_1 = require("../drivers/ios.js");
|
|
25
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
26
|
+
const ios_inproc_js_1 = require("../drivers/ios-inproc.js");
|
|
27
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
28
|
const android_js_1 = require("../drivers/android.js");
|
|
21
29
|
const web_js_1 = require("../drivers/web.js");
|
|
22
30
|
const vega_js_1 = require("../drivers/vega.js");
|
|
@@ -30,6 +38,7 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
|
|
|
30
38
|
process.stderr.write('warning: --clear-state / --clear-keychain wipes app data AND signed-in state; ' +
|
|
31
39
|
'the user will be signed out and cannot be recovered without their credentials.\n');
|
|
32
40
|
}
|
|
41
|
+
let injectionNote = '';
|
|
33
42
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
34
43
|
if (flags.clearKeychain)
|
|
35
44
|
await driver.clearKeychain();
|
|
@@ -47,7 +56,25 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
|
|
|
47
56
|
await driver.stopApp(appId);
|
|
48
57
|
}
|
|
49
58
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
50
|
-
|
|
59
|
+
if (flags.inject) {
|
|
60
|
+
const dylibPath = await (0, bootstrap_js_1.getInprocDylibPath)(driver.platform);
|
|
61
|
+
if (!fs_1.default.existsSync(dylibPath)) {
|
|
62
|
+
throw new Error(`--inject: in-process library not found at ${dylibPath}. ` +
|
|
63
|
+
`Build it with packages/ios-inproc/tools/build-inproc-dylib.sh`);
|
|
64
|
+
}
|
|
65
|
+
const deviceId = driver.deviceId;
|
|
66
|
+
if (!deviceId)
|
|
67
|
+
throw new Error('--inject requires a resolved iOS simulator device');
|
|
68
|
+
const inprocPort = (0, ios_inproc_js_1.getInprocPort)(deviceId);
|
|
69
|
+
await driver.launchApp(appId, flags.launchArgs, { dylibPath, inprocPort });
|
|
70
|
+
const ready = await new ios_inproc_js_1.InprocClient(inprocPort).waitUntilReady(10000);
|
|
71
|
+
injectionNote = ready
|
|
72
|
+
? ` (in-process control ready on :${inprocPort})`
|
|
73
|
+
: ` (warning: injected but in-process server did not answer on :${inprocPort})`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await driver.launchApp(appId, flags.launchArgs);
|
|
77
|
+
}
|
|
51
78
|
}
|
|
52
79
|
else if (driver instanceof web_js_1.WebDriver) {
|
|
53
80
|
await driver.launchApp(appId);
|
|
@@ -60,7 +87,7 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
|
|
|
60
87
|
}
|
|
61
88
|
}, sessionName);
|
|
62
89
|
if (result.success) {
|
|
63
|
-
(0, output_js_1.printSuccess)(`launch-app "${appId}" — done`, opts);
|
|
90
|
+
(0, output_js_1.printSuccess)(`launch-app "${appId}" — done${injectionNote}`, opts);
|
|
64
91
|
return 0;
|
|
65
92
|
}
|
|
66
93
|
else {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RN_PROPS_HELP = exports.RN_SET_HELP = void 0;
|
|
4
|
+
exports.nativeRnSet = nativeRnSet;
|
|
5
|
+
exports.nativeRnProps = nativeRnProps;
|
|
6
|
+
/**
|
|
7
|
+
* React Native prop live-editing over the Metro CDP (JS) channel.
|
|
8
|
+
*
|
|
9
|
+
* `RCTParagraphComponentView` and other Fabric host views have no working native
|
|
10
|
+
* setter and the injected dylib can't reach RN's C++ Fabric layer — so text/style
|
|
11
|
+
* edits must go through React itself. These commands drive React DevTools'
|
|
12
|
+
* `overrideProps` (edit) and read a fiber's `memoizedProps` (raw JSX props),
|
|
13
|
+
* reusing the same Metro CDP connection as `conductor debug`.
|
|
14
|
+
*
|
|
15
|
+
* Reads a component's reactTag from `native-inspect` (`rn.reactTag`).
|
|
16
|
+
*/
|
|
17
|
+
exports.RN_SET_HELP = ` native-rn-set --react-tag <n> --path <dot.path> --value <json> Live-edit an RN component's props via React (text, style.color, …; dev builds only)`;
|
|
18
|
+
exports.RN_PROPS_HELP = ` native-rn-props --react-tag <n> Raw JSX props (memoizedProps) of an RN fiber by reactTag`;
|
|
19
|
+
const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
|
|
20
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
21
|
+
const metro_scripts_js_1 = require("../drivers/metro-scripts.js");
|
|
22
|
+
const output_js_1 = require("../output.js");
|
|
23
|
+
/** Session → device/platform for Metro target selection (mirrors debug.ts). */
|
|
24
|
+
function resolveSession(sessionName) {
|
|
25
|
+
if (!sessionName || sessionName === 'default') {
|
|
26
|
+
return { deviceId: undefined, platformPromise: Promise.resolve(undefined) };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
deviceId: sessionName,
|
|
30
|
+
platformPromise: (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => undefined),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** JSON-parse a --value, falling back to the raw string (so bare text works). */
|
|
34
|
+
function parseValue(raw) {
|
|
35
|
+
try {
|
|
36
|
+
return { json: JSON.stringify(JSON.parse(raw)) };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { json: JSON.stringify(raw) };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function nativeRnSet(args, opts, sessionName, rnOpts) {
|
|
43
|
+
const tag = Number(args.reactTag);
|
|
44
|
+
if (!Number.isInteger(tag)) {
|
|
45
|
+
(0, output_js_1.printError)('native-rn-set needs --react-tag <n> (from native-inspect rn.reactTag)', opts);
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
if (!args.path) {
|
|
49
|
+
(0, output_js_1.printError)('native-rn-set needs --path <dot.path> (e.g. children, style.color)', opts);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
if (args.value === undefined) {
|
|
53
|
+
(0, output_js_1.printError)('native-rn-set needs --value <json> (JSON; a bare string is fine for text)', opts);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const path = args.path.split('.').filter((s) => s.length > 0);
|
|
57
|
+
const { json } = parseValue(args.value);
|
|
58
|
+
const port = rnOpts.port ?? 8081;
|
|
59
|
+
const { deviceId, platformPromise } = resolveSession(sessionName);
|
|
60
|
+
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
61
|
+
try {
|
|
62
|
+
const platform = await platformPromise;
|
|
63
|
+
await client.connect({ port, deviceId, platform, targetIndex: rnOpts.targetIndex });
|
|
64
|
+
const raw = await client.evaluate((0, metro_scripts_js_1.makeOverridePropsScript)(tag, path, json), true);
|
|
65
|
+
const res = JSON.parse(raw);
|
|
66
|
+
client.close();
|
|
67
|
+
if (res.status !== 'ok') {
|
|
68
|
+
(0, output_js_1.printError)(res.message ?? 'overrideProps failed', opts);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const out = { status: 'ok', applied: res.applied ?? true };
|
|
72
|
+
if (res.note)
|
|
73
|
+
out.note = res.note;
|
|
74
|
+
(0, output_js_1.printData)(out, opts);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
client.close();
|
|
79
|
+
(0, output_js_1.printError)(`native-rn-set — ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function nativeRnProps(args, opts, sessionName, rnOpts) {
|
|
84
|
+
const tag = Number(args.reactTag);
|
|
85
|
+
if (!Number.isInteger(tag)) {
|
|
86
|
+
(0, output_js_1.printError)('native-rn-props needs --react-tag <n> (from native-inspect rn.reactTag)', opts);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const port = rnOpts.port ?? 8081;
|
|
90
|
+
const { deviceId, platformPromise } = resolveSession(sessionName);
|
|
91
|
+
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
92
|
+
try {
|
|
93
|
+
const platform = await platformPromise;
|
|
94
|
+
await client.connect({ port, deviceId, platform, targetIndex: rnOpts.targetIndex });
|
|
95
|
+
const raw = await client.evaluate((0, metro_scripts_js_1.makeRnPropsScript)(tag), true);
|
|
96
|
+
const res = JSON.parse(raw);
|
|
97
|
+
client.close();
|
|
98
|
+
if (res.status !== 'ok') {
|
|
99
|
+
(0, output_js_1.printError)(res.message ?? 'reading props failed', opts);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
(0, output_js_1.printData)({ status: 'ok', props: res.props ?? {} }, opts);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
client.close();
|
|
107
|
+
(0, output_js_1.printError)(`native-rn-props — ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FIND_HELP = exports.HIGHLIGHT_HELP = exports.HITTEST_HELP = exports.CONSTRAINTS_HELP = exports.PROPS_HELP = exports.SET_HELP = exports.VIEW_HELP = exports.EVAL_HELP = exports.APPEARANCE_HELP = exports.HEAP_HELP = exports.NETWORK_HELP = exports.CONSOLE_HELP = exports.RAW_HELP = exports.SNAPSHOT_HELP = exports.IMAGE_HELP = exports.SCREENSHOT_HELP = exports.NAV_HELP = exports.INSPECT_HELP = exports.PING_HELP = void 0;
|
|
7
|
+
exports.nativePing = nativePing;
|
|
8
|
+
exports.nativeInspect = nativeInspect;
|
|
9
|
+
exports.nativeNav = nativeNav;
|
|
10
|
+
exports.nativeScreenshot = nativeScreenshot;
|
|
11
|
+
exports.nativeImage = nativeImage;
|
|
12
|
+
exports.nativeRaw = nativeRaw;
|
|
13
|
+
exports.nativeConsole = nativeConsole;
|
|
14
|
+
exports.nativeNetwork = nativeNetwork;
|
|
15
|
+
exports.nativeHeap = nativeHeap;
|
|
16
|
+
exports.nativeAppearance = nativeAppearance;
|
|
17
|
+
exports.nativeEval = nativeEval;
|
|
18
|
+
exports.nativeSnapshot = nativeSnapshot;
|
|
19
|
+
exports.nativeView = nativeView;
|
|
20
|
+
exports.nativeSet = nativeSet;
|
|
21
|
+
exports.nativeProps = nativeProps;
|
|
22
|
+
exports.nativeConstraints = nativeConstraints;
|
|
23
|
+
exports.nativeHittest = nativeHittest;
|
|
24
|
+
exports.nativeHighlight = nativeHighlight;
|
|
25
|
+
exports.nativeFind = nativeFind;
|
|
26
|
+
// iOS/tvOS simulator only; the app must be launched with `launch-app --inject`.
|
|
27
|
+
exports.PING_HELP = ` native-ping Check the injected in-process control library is alive`;
|
|
28
|
+
exports.INSPECT_HELP = ` native-inspect Full native view tree: colors, fonts, text, layer visuals, absFrame`;
|
|
29
|
+
exports.NAV_HELP = ` native-nav Navigation state: view-controller stacks, tabs, presented`;
|
|
30
|
+
exports.SCREENSHOT_HELP = ` native-screenshot --output <p.png> In-process PNG of the key window`;
|
|
31
|
+
exports.IMAGE_HELP = ` native-image <x,y,w,h> --output <p> PNG crop of a window-absolute rect (use a node's absFrame)`;
|
|
32
|
+
exports.SNAPSHOT_HELP = ` native-snapshot <id> --output <p> Isolated PNG of one view (own content, transparent) for 3D-explosion layers; --with-subviews for the composited view`;
|
|
33
|
+
exports.RAW_HELP = ` native-raw <path> GET any in-process endpoint (e.g. '/get?id=..&keyPath=layer.cornerRadius'); --output <f> saves image endpoints. See packages/ios-inproc/README.md for the full list`;
|
|
34
|
+
exports.CONSOLE_HELP = ` native-console [--since <n>] App stdout/stderr + logs (poll with the returned cursor)`;
|
|
35
|
+
exports.NETWORK_HELP = ` native-network [--since <n>] Captured HTTP requests/responses (poll with cursor)`;
|
|
36
|
+
exports.HEAP_HELP = ` native-heap --class <name> | --pattern <s> | --read <addr> [--key <keyPath>] Live object browser`;
|
|
37
|
+
exports.APPEARANCE_HELP = ` native-appearance <light|dark|system> | --direction <ltr|rtl> | --content-size <cat> | --anim-speed <n>`;
|
|
38
|
+
exports.EVAL_HELP = ` native-eval <swift> Compile & run arbitrary Swift inside the app; --mode full for a whole function body. e.g. native-eval 'UIApplication.shared.connectedScenes.count'`;
|
|
39
|
+
exports.VIEW_HELP = ` native-view <id> Full property detail for a view (id from native-inspect)`;
|
|
40
|
+
exports.SET_HELP = ` native-set <id> <key> <value> Live-edit a view property (alpha, hidden, backgroundColor, tintColor, cornerRadius, borderWidth, borderColor, frame, text, textColor). text/textColor also apply to RN Fabric text`;
|
|
41
|
+
exports.PROPS_HELP = ` native-props <id> React Native Fabric props: typed ViewProps + raw JS prop bag (Fabric views only)`;
|
|
42
|
+
exports.CONSTRAINTS_HELP = ` native-constraints <id> Auto Layout constraints affecting a view + ambiguity`;
|
|
43
|
+
exports.HITTEST_HELP = ` native-hittest <x,y> Topmost view at a screen point + ancestor chain`;
|
|
44
|
+
exports.HIGHLIGHT_HELP = ` native-highlight <id> Flash a highlight over the view on the device`;
|
|
45
|
+
exports.FIND_HELP = ` native-find [--class <name>] [--text <s>] Search views by class and/or text`;
|
|
46
|
+
const fs_1 = __importDefault(require("fs"));
|
|
47
|
+
const runner_js_1 = require("../runner.js");
|
|
48
|
+
const session_js_1 = require("../session.js");
|
|
49
|
+
const output_js_1 = require("../output.js");
|
|
50
|
+
const ios_js_1 = require("../drivers/ios.js");
|
|
51
|
+
const ios_inproc_js_1 = require("../drivers/ios-inproc.js");
|
|
52
|
+
const eval_compiler_js_1 = require("../drivers/eval-compiler.js");
|
|
53
|
+
/** Resolve the in-process client for the current iOS/tvOS session, or print why not. */
|
|
54
|
+
async function resolveClient(sessionName, opts) {
|
|
55
|
+
const driver = await (0, runner_js_1.getDriver)(sessionName);
|
|
56
|
+
if (!(driver instanceof ios_js_1.IOSDriver)) {
|
|
57
|
+
(0, output_js_1.printError)('native commands are iOS/tvOS simulator only', opts);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const deviceId = driver.deviceId;
|
|
61
|
+
if (!deviceId) {
|
|
62
|
+
(0, output_js_1.printError)('native commands require a resolved simulator device', opts);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return new ios_inproc_js_1.InprocClient((0, ios_inproc_js_1.getInprocPort)(deviceId));
|
|
66
|
+
}
|
|
67
|
+
async function nativePing(opts = {}, sessionName = 'default') {
|
|
68
|
+
try {
|
|
69
|
+
const client = await resolveClient(sessionName, opts);
|
|
70
|
+
if (!client)
|
|
71
|
+
return 1;
|
|
72
|
+
const res = await client.ping().catch(() => null);
|
|
73
|
+
if (!res || res.status !== 'ok') {
|
|
74
|
+
(0, output_js_1.printError)('no in-process server. Launch the app with: launch-app <appId> --inject', opts);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
if (opts.json) {
|
|
78
|
+
console.log(JSON.stringify({ ...res, status: 'ok' }));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
(0, output_js_1.printSuccess)(`in-process control alive — pid ${res.pid}, app ${res.app}`, opts);
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
(0, output_js_1.printError)(`native-ping failed: ${err.message}`, opts);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function nativeInspect(opts = {}, sessionName = 'default') {
|
|
91
|
+
return runQuery('native-inspect', (c) => c.inspect(), opts, sessionName);
|
|
92
|
+
}
|
|
93
|
+
async function nativeNav(opts = {}, sessionName = 'default') {
|
|
94
|
+
return runQuery('native-nav', (c) => c.nav(), opts, sessionName);
|
|
95
|
+
}
|
|
96
|
+
async function nativeScreenshot(output, opts = {}, sessionName = 'default') {
|
|
97
|
+
return saveImage('native-screenshot', (c) => c.screenshot(), output, opts, sessionName);
|
|
98
|
+
}
|
|
99
|
+
async function nativeImage(frameArg, output, opts = {}, sessionName = 'default') {
|
|
100
|
+
const parts = (frameArg ?? '').split(',').map((n) => Number(n.trim()));
|
|
101
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
|
|
102
|
+
(0, output_js_1.printError)('native-image needs a rect: native-image <x,y,w,h> --output <file>', opts);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const [x, y, w, h] = parts;
|
|
106
|
+
return saveImage('native-image', (c) => c.image({ x, y, w, h }), output, opts, sessionName);
|
|
107
|
+
}
|
|
108
|
+
async function nativeRaw(reqPath, output, opts = {}, sessionName = 'default') {
|
|
109
|
+
if (!reqPath) {
|
|
110
|
+
(0, output_js_1.printError)("native-raw needs a path, e.g. native-raw '/get?id=0x..&keyPath=alpha'", opts);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const client = await resolveClient(sessionName, opts);
|
|
115
|
+
if (!client)
|
|
116
|
+
return 1;
|
|
117
|
+
const { contentType, body } = await client.rawRequest(reqPath);
|
|
118
|
+
if (contentType.startsWith('image/')) {
|
|
119
|
+
const dest = output ?? `native-raw-${Date.now()}.png`;
|
|
120
|
+
fs_1.default.writeFileSync(dest, body);
|
|
121
|
+
(0, output_js_1.printSuccess)(`saved ${body.length} bytes → ${dest}`, opts);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Pretty-print JSON when possible.
|
|
125
|
+
const text = body.toString('utf-8');
|
|
126
|
+
try {
|
|
127
|
+
console.log(JSON.stringify(JSON.parse(text), null, opts.json ? 0 : 2));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
console.log(text);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
(0, output_js_1.printError)(`native-raw failed: ${err.message}. Is the app launched with --inject?`, opts);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function nativeConsole(since, opts = {}, sessionName = 'default') {
|
|
141
|
+
return runQuery('native-console', (c) => c.rawJson(`/console?since=${since ?? 0}`), opts, sessionName);
|
|
142
|
+
}
|
|
143
|
+
async function nativeNetwork(since, opts = {}, sessionName = 'default') {
|
|
144
|
+
return runQuery('native-network', (c) => c.rawJson(`/network?since=${since ?? 0}`), opts, sessionName);
|
|
145
|
+
}
|
|
146
|
+
async function nativeHeap(filters, opts = {}, sessionName = 'default') {
|
|
147
|
+
let path;
|
|
148
|
+
if (filters.read) {
|
|
149
|
+
path = `/heap/read?address=${encodeURIComponent(filters.read)}`;
|
|
150
|
+
if (filters.key)
|
|
151
|
+
path += `&keyPath=${encodeURIComponent(filters.key)}`;
|
|
152
|
+
}
|
|
153
|
+
else if (filters.className) {
|
|
154
|
+
path = `/heap/instances?class=${encodeURIComponent(filters.className)}`;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
path = `/heap/classes?pattern=${encodeURIComponent(filters.pattern ?? '')}`;
|
|
158
|
+
}
|
|
159
|
+
return runQuery('native-heap', (c) => c.rawJson(path), opts, sessionName);
|
|
160
|
+
}
|
|
161
|
+
async function nativeAppearance(args, opts = {}, sessionName = 'default') {
|
|
162
|
+
let path;
|
|
163
|
+
if (args.direction)
|
|
164
|
+
path = `/direction?direction=${encodeURIComponent(args.direction)}`;
|
|
165
|
+
else if (args.contentSize)
|
|
166
|
+
path = `/contentsize?category=${encodeURIComponent(args.contentSize)}`;
|
|
167
|
+
else if (args.animSpeed)
|
|
168
|
+
path = `/animspeed?speed=${encodeURIComponent(args.animSpeed)}`;
|
|
169
|
+
else if (args.style)
|
|
170
|
+
path = `/appearance?style=${encodeURIComponent(args.style)}`;
|
|
171
|
+
else {
|
|
172
|
+
(0, output_js_1.printError)('native-appearance needs <light|dark|system> or --direction/--content-size/--anim-speed', opts);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
return runQuery('native-appearance', (c) => c.rawJson(path), opts, sessionName);
|
|
176
|
+
}
|
|
177
|
+
async function nativeEval(code, mode, opts = {}, sessionName = 'default') {
|
|
178
|
+
if (!code) {
|
|
179
|
+
(0, output_js_1.printError)("native-eval needs Swift code, e.g. native-eval 'UIScreen.main.bounds'", opts);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const driver = await (0, runner_js_1.getDriver)(sessionName);
|
|
184
|
+
if (!(driver instanceof ios_js_1.IOSDriver)) {
|
|
185
|
+
(0, output_js_1.printError)('native-eval is iOS/tvOS simulator only', opts);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
const deviceId = driver.deviceId;
|
|
189
|
+
const bundleId = (await (0, session_js_1.getSession)(sessionName)).appId;
|
|
190
|
+
if (!deviceId || !bundleId) {
|
|
191
|
+
(0, output_js_1.printError)('native-eval needs a resolved device and a launched app (launch-app <appId> --inject)', opts);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
const compiled = await (0, eval_compiler_js_1.compileEval)(code, mode, driver.platform, deviceId, bundleId);
|
|
195
|
+
if (!compiled.ok || !compiled.dylibPath) {
|
|
196
|
+
(0, output_js_1.printError)(compiled.error ?? 'compile failed', opts);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
const client = new ios_inproc_js_1.InprocClient((0, ios_inproc_js_1.getInprocPort)(deviceId));
|
|
200
|
+
const res = await client
|
|
201
|
+
.rawJson(`/eval?dylib=${encodeURIComponent(compiled.dylibPath)}`, 30000)
|
|
202
|
+
.catch((err) => {
|
|
203
|
+
throw new Error(`${err.message}. Is the app running with launch-app --inject?`);
|
|
204
|
+
});
|
|
205
|
+
if (res.status !== 'ok') {
|
|
206
|
+
(0, output_js_1.printError)(`eval error: ${res.message ?? JSON.stringify(res)}`, opts);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
if (opts.json) {
|
|
210
|
+
console.log(JSON.stringify({ status: 'ok', result: res.result, info: compiled.info }));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
(0, output_js_1.printSuccess)(`[${compiled.info}] ${res.result}`, opts);
|
|
214
|
+
}
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
(0, output_js_1.printError)(`native-eval failed: ${err.message}`, opts);
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function nativeSnapshot(id, withSubviews, output, opts = {}, sessionName = 'default') {
|
|
223
|
+
if (!id) {
|
|
224
|
+
(0, output_js_1.printError)('native-snapshot needs a view id (from native-inspect)', opts);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
return saveImage('native-snapshot', (c) => c.snapshot(id, withSubviews), output, opts, sessionName);
|
|
228
|
+
}
|
|
229
|
+
async function nativeView(id, opts = {}, sessionName = 'default') {
|
|
230
|
+
if (!id) {
|
|
231
|
+
(0, output_js_1.printError)('native-view needs a view id (from native-inspect)', opts);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
return runQuery('native-view', (c) => c.view(id), opts, sessionName);
|
|
235
|
+
}
|
|
236
|
+
async function nativeSet(args, opts = {}, sessionName = 'default') {
|
|
237
|
+
const [id, key, ...valueParts] = args;
|
|
238
|
+
const value = valueParts.join(' ');
|
|
239
|
+
if (!id || !key || value === '') {
|
|
240
|
+
(0, output_js_1.printError)('usage: native-set <id> <key> <value>', opts);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
return runQuery('native-set', (c) => c.set(id, key, value), opts, sessionName);
|
|
244
|
+
}
|
|
245
|
+
async function nativeProps(id, opts = {}, sessionName = 'default') {
|
|
246
|
+
if (!id) {
|
|
247
|
+
(0, output_js_1.printError)('native-props needs a view id (from native-inspect)', opts);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
return runQuery('native-props', (c) => c.props(id), opts, sessionName);
|
|
251
|
+
}
|
|
252
|
+
async function nativeConstraints(id, opts = {}, sessionName = 'default') {
|
|
253
|
+
if (!id) {
|
|
254
|
+
(0, output_js_1.printError)('native-constraints needs a view id', opts);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
return runQuery('native-constraints', (c) => c.constraints(id), opts, sessionName);
|
|
258
|
+
}
|
|
259
|
+
async function nativeHittest(point, opts = {}, sessionName = 'default') {
|
|
260
|
+
const [x, y] = (point ?? '').split(',').map((n) => Number(n.trim()));
|
|
261
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
262
|
+
(0, output_js_1.printError)('usage: native-hittest <x,y>', opts);
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
return runQuery('native-hittest', (c) => c.hittest(x, y), opts, sessionName);
|
|
266
|
+
}
|
|
267
|
+
async function nativeHighlight(id, opts = {}, sessionName = 'default') {
|
|
268
|
+
if (!id) {
|
|
269
|
+
(0, output_js_1.printError)('native-highlight needs a view id', opts);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
return runQuery('native-highlight', (c) => c.highlight(id), opts, sessionName);
|
|
273
|
+
}
|
|
274
|
+
async function nativeFind(filters, opts = {}, sessionName = 'default') {
|
|
275
|
+
if (!filters.className && !filters.text) {
|
|
276
|
+
(0, output_js_1.printError)('native-find needs --class <name> and/or --text <substring>', opts);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
return runQuery('native-find', (c) => c.find(filters), opts, sessionName);
|
|
280
|
+
}
|
|
281
|
+
async function saveImage(label, fetch, output, opts, sessionName) {
|
|
282
|
+
try {
|
|
283
|
+
const client = await resolveClient(sessionName, opts);
|
|
284
|
+
if (!client)
|
|
285
|
+
return 1;
|
|
286
|
+
const png = await fetch(client).catch((err) => {
|
|
287
|
+
throw new Error(`${err.message}. Is the app running with launch-app --inject?`);
|
|
288
|
+
});
|
|
289
|
+
const dest = output ?? `${label}-${Date.now()}.png`;
|
|
290
|
+
fs_1.default.writeFileSync(dest, png);
|
|
291
|
+
if (opts.json) {
|
|
292
|
+
console.log(JSON.stringify({ status: 'ok', output: dest, bytes: png.length }));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
(0, output_js_1.printSuccess)(`${label} → ${dest} (${png.length} bytes)`, opts);
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
(0, output_js_1.printError)(`${label} failed: ${err.message}`, opts);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function runQuery(label, query, opts, sessionName) {
|
|
305
|
+
try {
|
|
306
|
+
const client = await resolveClient(sessionName, opts);
|
|
307
|
+
if (!client)
|
|
308
|
+
return 1;
|
|
309
|
+
const res = await query(client).catch((err) => {
|
|
310
|
+
throw new Error(`${err.message}. Is the app running with launch-app --inject?`);
|
|
311
|
+
});
|
|
312
|
+
// Always emit JSON — these payloads are structured data for the agent.
|
|
313
|
+
console.log(JSON.stringify(res, null, opts.json ? 0 : 2));
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
(0, output_js_1.printError)(`${label} failed: ${err.message}`, opts);
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
package/dist/daemon/client.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fetchDaemonStatus = fetchDaemonStatus;
|
|
6
7
|
exports.startDaemon = startDaemon;
|
|
7
8
|
exports.stopDaemon = stopDaemon;
|
|
8
9
|
exports.listDaemonSessions = listDaemonSessions;
|
|
@@ -19,6 +20,10 @@ const verbose_js_1 = require("../verbose.js");
|
|
|
19
20
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
20
21
|
const STARTUP_POLL_MS = 200;
|
|
21
22
|
const STARTUP_MAX_WAIT_MS = 10000;
|
|
23
|
+
/** Full daemon `/status` body for a session, or null if the daemon isn't reachable. */
|
|
24
|
+
async function fetchDaemonStatus(sessionName) {
|
|
25
|
+
return fetchStatus(sessionName);
|
|
26
|
+
}
|
|
22
27
|
async function fetchStatus(sessionName) {
|
|
23
28
|
return new Promise((resolve) => {
|
|
24
29
|
const req = http_1.default.get({ socketPath: (0, protocol_js_1.socketPath)(sessionName), path: '/status' }, (res) => {
|