@houwert/conductor 0.24.0 → 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/commands/start-device.js +62 -4
- 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/daemon/web-server.js +80 -79
- 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 +142 -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 +2 -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
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.pickAndroidArch = pickAndroidArch;
|
|
|
8
8
|
exports.parseInstalledSystemImages = parseInstalledSystemImages;
|
|
9
9
|
exports.pickSystemImage = pickSystemImage;
|
|
10
10
|
exports.buildAvdmanagerCreateArgs = buildAvdmanagerCreateArgs;
|
|
11
|
+
exports.raiseAvdConfigRam = raiseAvdConfigRam;
|
|
11
12
|
exports.startDevice = startDevice;
|
|
12
13
|
exports.HELP = ` start-device
|
|
13
14
|
--platform <ios|android|tvos|web|vega> Boot a simulator/emulator, start the web driver (Playwright), or boot/attach a Vega VVD
|
|
@@ -16,10 +17,14 @@ exports.HELP = ` start-device
|
|
|
16
17
|
--name <name> Set a custom name on the device after creation (iOS/tvOS/web)
|
|
17
18
|
--device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K") or
|
|
18
19
|
Android device profile (e.g. "pixel_7"); creates if needed
|
|
20
|
+
--memory <mb> Android only: RAM (MB) for a newly-created AVD (default: 4096).
|
|
21
|
+
Only raises; applied at creation time, never to an existing AVD
|
|
19
22
|
--system-image <id> Android only: override auto-picked system image
|
|
20
23
|
(e.g. "system-images;android-34;google_apis;arm64-v8a")
|
|
21
24
|
--browser <chromium|firefox|webkit> Web only: which Playwright browser to launch (default: chromium)`;
|
|
22
25
|
const fs_1 = __importDefault(require("fs"));
|
|
26
|
+
const os_1 = __importDefault(require("os"));
|
|
27
|
+
const path_1 = __importDefault(require("path"));
|
|
23
28
|
const child_process_1 = require("child_process");
|
|
24
29
|
const runner_js_1 = require("../runner.js");
|
|
25
30
|
const sdk_js_1 = require("../android/sdk.js");
|
|
@@ -32,6 +37,10 @@ const cli_js_1 = require("../drivers/vega/cli.js");
|
|
|
32
37
|
const IOS_BOOT_TIMEOUT_MS = 120000;
|
|
33
38
|
const ANDROID_BOOT_TIMEOUT_MS = 120000;
|
|
34
39
|
const POLL_MS = 1000;
|
|
40
|
+
// Stock TV device profiles default to 1024MB RAM, which OOM-kills heavy RN debug
|
|
41
|
+
// builds during JS bundle load. Raise freshly-created AVDs to this floor.
|
|
42
|
+
const ANDROID_AVD_RAM_FLOOR_MB = 4096;
|
|
43
|
+
const ANDROID_AVD_HEAP_MB = 512;
|
|
35
44
|
async function listIOSSimulators() {
|
|
36
45
|
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'list', 'devices', '--json']);
|
|
37
46
|
if (!result.success)
|
|
@@ -496,6 +505,38 @@ function pickSystemImage(installed, apiLevel, arch) {
|
|
|
496
505
|
function buildAvdmanagerCreateArgs(avdName, systemImage, deviceProfile) {
|
|
497
506
|
return ['create', 'avd', '-n', avdName, '-k', systemImage, '-d', deviceProfile];
|
|
498
507
|
}
|
|
508
|
+
/** Parse a config.ini RAM value; only plain integer MB counts — `M`-suffixed or bogus values are 0. */
|
|
509
|
+
function parsePlainMb(value) {
|
|
510
|
+
const m = /^(\d+)$/.exec(value.trim());
|
|
511
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Rewrite an AVD `config.ini` to raise `hw.ramSize`/`vm.heapSize` to the given floors.
|
|
515
|
+
* Pure and exported for tests. Values are written as plain integer MB — an `M` suffix
|
|
516
|
+
* makes the emulator silently fall back to 1024MB. Only ever raises; higher existing
|
|
517
|
+
* values are kept, and non-integer/`M`-suffixed existing values are treated as 0 so they
|
|
518
|
+
* get overwritten. Missing keys are appended; all other lines/ordering are preserved.
|
|
519
|
+
*/
|
|
520
|
+
function raiseAvdConfigRam(configIni, targetRamMb, heapMb = ANDROID_AVD_HEAP_MB) {
|
|
521
|
+
const raiseKey = (contents, key, target) => {
|
|
522
|
+
const lines = contents.split('\n');
|
|
523
|
+
for (let i = 0; i < lines.length; i++) {
|
|
524
|
+
const m = /^(\s*)([^=\s]+)(\s*=\s*)(.*)$/.exec(lines[i]);
|
|
525
|
+
if (!m || m[2] !== key)
|
|
526
|
+
continue;
|
|
527
|
+
const next = Math.max(parsePlainMb(m[4]), target);
|
|
528
|
+
lines[i] = `${m[1]}${key}${m[3]}${next}`;
|
|
529
|
+
return lines.join('\n');
|
|
530
|
+
}
|
|
531
|
+
let out = contents;
|
|
532
|
+
if (out.length > 0 && !out.endsWith('\n'))
|
|
533
|
+
out += '\n';
|
|
534
|
+
return `${out}${key} = ${target}\n`;
|
|
535
|
+
};
|
|
536
|
+
let result = raiseKey(configIni, 'hw.ramSize', targetRamMb);
|
|
537
|
+
result = raiseKey(result, 'vm.heapSize', heapMb);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
499
540
|
async function listInstalledSystemImages() {
|
|
500
541
|
const result = await (0, runner_js_1.spawnCommand)('sdkmanager', ['--list_installed']);
|
|
501
542
|
if (!result.success) {
|
|
@@ -524,7 +565,7 @@ async function spawnAvdmanagerCreate(args) {
|
|
|
524
565
|
proc.stdin.end('no\n');
|
|
525
566
|
});
|
|
526
567
|
}
|
|
527
|
-
async function createAndroidAVD(avdName, deviceProfile, apiLevel, systemImageOverride) {
|
|
568
|
+
async function createAndroidAVD(avdName, deviceProfile, apiLevel, systemImageOverride, targetRamMb) {
|
|
528
569
|
const arch = pickAndroidArch();
|
|
529
570
|
let systemImage;
|
|
530
571
|
if (systemImageOverride) {
|
|
@@ -562,6 +603,22 @@ async function createAndroidAVD(avdName, deviceProfile, apiLevel, systemImageOve
|
|
|
562
603
|
if (!avds.includes(avdName)) {
|
|
563
604
|
throw new Error(`AVD "${avdName}" was not registered after creation (not in emulator -list-avds).`);
|
|
564
605
|
}
|
|
606
|
+
// Raise RAM above the stock profile default so heavy RN debug builds aren't OOM-killed.
|
|
607
|
+
const avdHome = process.env.ANDROID_AVD_HOME || path_1.default.join(os_1.default.homedir(), '.android', 'avd');
|
|
608
|
+
const configPath = path_1.default.join(avdHome, `${avdName}.avd`, 'config.ini');
|
|
609
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
610
|
+
try {
|
|
611
|
+
const current = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
612
|
+
fs_1.default.writeFileSync(configPath, raiseAvdConfigRam(current, targetRamMb), 'utf-8');
|
|
613
|
+
}
|
|
614
|
+
catch (e) {
|
|
615
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
616
|
+
console.warn(`Warning: could not raise RAM for AVD "${avdName}" (${msg}). Continuing.`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
console.warn(`Warning: config.ini not found for AVD "${avdName}" at ${configPath}. Continuing.`);
|
|
621
|
+
}
|
|
565
622
|
}
|
|
566
623
|
async function waitForAndroidBoot(avdName) {
|
|
567
624
|
const deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;
|
|
@@ -596,7 +653,7 @@ async function waitForAndroidBoot(avdName) {
|
|
|
596
653
|
}
|
|
597
654
|
throw new Error(`Android emulator (${avdName}) did not appear within ${ANDROID_BOOT_TIMEOUT_MS / 1000}s`);
|
|
598
655
|
}
|
|
599
|
-
async function startAndroid(avdName, opts, deviceType, osVersion, systemImage) {
|
|
656
|
+
async function startAndroid(avdName, opts, deviceType, osVersion, systemImage, memory) {
|
|
600
657
|
let avds;
|
|
601
658
|
try {
|
|
602
659
|
avds = await listAVDs();
|
|
@@ -616,8 +673,9 @@ async function startAndroid(avdName, opts, deviceType, osVersion, systemImage) {
|
|
|
616
673
|
return 1;
|
|
617
674
|
}
|
|
618
675
|
console.log(`No AVD "${avdName}" found. Creating one with device profile "${deviceType}"...`);
|
|
676
|
+
const targetRamMb = memory && memory > 0 ? memory : ANDROID_AVD_RAM_FLOOR_MB;
|
|
619
677
|
try {
|
|
620
|
-
await createAndroidAVD(avdName, deviceType, osVersion, systemImage);
|
|
678
|
+
await createAndroidAVD(avdName, deviceType, osVersion, systemImage, targetRamMb);
|
|
621
679
|
}
|
|
622
680
|
catch (e) {
|
|
623
681
|
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
@@ -761,7 +819,7 @@ async function startDevice(platform, opts, flags) {
|
|
|
761
819
|
case 'tvos':
|
|
762
820
|
return startTvOS(flags.osVersion, opts, flags.name, flags.deviceType);
|
|
763
821
|
case 'android':
|
|
764
|
-
return startAndroid(flags.avd, opts, flags.deviceType, flags.osVersion, flags.systemImage);
|
|
822
|
+
return startAndroid(flags.avd, opts, flags.deviceType, flags.osVersion, flags.systemImage, flags.memory);
|
|
765
823
|
case 'web':
|
|
766
824
|
return startWebDriver(opts, flags.browser, flags.name);
|
|
767
825
|
case 'vega':
|