@houwert/conductor 0.7.1 → 0.9.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/delete-device.js +64 -2
- package/dist/commands/download-app.js +4 -0
- package/dist/commands/install-app.js +5 -1
- package/dist/commands/list-apps.js +5 -1
- package/dist/commands/list-devices.js +28 -20
- package/dist/commands/start-device.js +33 -11
- package/dist/commands/stop-device.js +113 -0
- package/dist/daemon/client.js +6 -1
- package/dist/daemon/protocol.js +4 -0
- package/dist/daemon/server.js +55 -1
- package/dist/daemon/web-server.js +130 -15
- package/dist/drivers/bootstrap.js +6 -1
- package/dist/index.js +9 -0
- package/dist/runner.js +11 -0
- package/drivers/ios/conductor-driver-ios.zip +0 -0
- package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
- package/drivers/tvos/conductor-driver-tvos.zip +0 -0
- package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
- package/package.json +1 -1
- package/skills/conductor/SKILL.md +1 -1
- package/skills/skills.yaml +1 -1
|
@@ -1,12 +1,19 @@
|
|
|
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.deleteDevice = deleteDevice;
|
|
5
8
|
exports.HELP = ` delete-device <name-or-id>
|
|
6
|
-
--platform <ios|tvos|android>
|
|
7
|
-
--all
|
|
9
|
+
--platform <ios|tvos|android|web> Scope to a single platform
|
|
10
|
+
--all Delete all shutdown simulators / non-running AVDs / web sessions`;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
12
|
const runner_js_1 = require("../runner.js");
|
|
9
13
|
const output_js_1 = require("../output.js");
|
|
14
|
+
const client_js_1 = require("../daemon/client.js");
|
|
15
|
+
const protocol_js_1 = require("../daemon/protocol.js");
|
|
16
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
10
17
|
async function listSimulators() {
|
|
11
18
|
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'list', 'devices', '--json']);
|
|
12
19
|
if (!result.success)
|
|
@@ -89,6 +96,7 @@ async function deleteDevice(nameOrId, opts, flags) {
|
|
|
89
96
|
const includeIOS = !platform || platform === 'ios';
|
|
90
97
|
const includeTvOS = !platform || platform === 'tvos';
|
|
91
98
|
const includeAndroid = !platform || platform === 'android';
|
|
99
|
+
const includeWeb = !platform || platform === 'web';
|
|
92
100
|
const deleted = [];
|
|
93
101
|
// ── --all mode ───────────────────────────────────────────────────────────
|
|
94
102
|
if (flags.all) {
|
|
@@ -135,6 +143,26 @@ async function deleteDevice(nameOrId, opts, flags) {
|
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
}
|
|
146
|
+
// Web: stop all running web daemon sessions
|
|
147
|
+
if (includeWeb) {
|
|
148
|
+
const sessions = (0, client_js_1.listDaemonSessions)();
|
|
149
|
+
for (const session of sessions) {
|
|
150
|
+
if (!(session === 'web' || session.startsWith('web:')))
|
|
151
|
+
continue;
|
|
152
|
+
const status = await (0, client_js_1.daemonStatus)(session);
|
|
153
|
+
if (!status.running)
|
|
154
|
+
continue;
|
|
155
|
+
try {
|
|
156
|
+
await (0, client_js_1.stopDaemon)(session);
|
|
157
|
+
const browser = (0, bootstrap_js_1.webBrowserName)(session);
|
|
158
|
+
const label = browser.charAt(0).toUpperCase() + browser.slice(1);
|
|
159
|
+
deleted.push({ id: session, name: label, platform: 'web' });
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
(0, output_js_1.printError)(`Failed to stop web session ${session}: ${e instanceof Error ? e.message : String(e)}`, opts);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
138
166
|
if (deleted.length === 0) {
|
|
139
167
|
(0, output_js_1.printError)('No devices to delete.', opts);
|
|
140
168
|
return 1;
|
|
@@ -217,6 +245,40 @@ async function deleteDevice(nameOrId, opts, flags) {
|
|
|
217
245
|
return 0;
|
|
218
246
|
}
|
|
219
247
|
}
|
|
248
|
+
// Try Web session (match by session ID or persisted name)
|
|
249
|
+
if (includeWeb && nameOrId) {
|
|
250
|
+
const sessions = (0, client_js_1.listDaemonSessions)();
|
|
251
|
+
const match = sessions.find((s) => {
|
|
252
|
+
if (!(s === 'web' || s.startsWith('web:')))
|
|
253
|
+
return false;
|
|
254
|
+
if (s === nameOrId)
|
|
255
|
+
return true;
|
|
256
|
+
try {
|
|
257
|
+
return fs_1.default.readFileSync((0, protocol_js_1.nameFile)(s), 'utf-8').trim() === nameOrId;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
if (match) {
|
|
264
|
+
try {
|
|
265
|
+
await (0, client_js_1.stopDaemon)(match);
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
const browser = (0, bootstrap_js_1.webBrowserName)(match);
|
|
272
|
+
const label = browser.charAt(0).toUpperCase() + browser.slice(1);
|
|
273
|
+
if (opts.json) {
|
|
274
|
+
(0, output_js_1.printData)({ status: 'ok', deleted: [{ id: match, name: label, platform: 'web' }] }, opts);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
(0, output_js_1.printSuccess)(`Stopped web session: ${label} (${match})`, opts);
|
|
278
|
+
}
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
220
282
|
(0, output_js_1.printError)(`Device "${nameOrId}" not found.`, opts);
|
|
221
283
|
return 1;
|
|
222
284
|
}
|
|
@@ -28,6 +28,10 @@ async function downloadApp(appId, output, opts = {}, sessionName = 'default') {
|
|
|
28
28
|
return 1;
|
|
29
29
|
}
|
|
30
30
|
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
31
|
+
if (platform === 'web') {
|
|
32
|
+
(0, output_js_1.printError)('download-app is not supported on web.', opts);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
31
35
|
if (platform === 'ios' || platform === 'tvos') {
|
|
32
36
|
// Get the .app bundle path from the simulator
|
|
33
37
|
const getPath = await (0, runner_js_1.spawnCommand)('xcrun', [
|
|
@@ -24,7 +24,11 @@ async function installApp(appPath, opts = {}, sessionName = 'default') {
|
|
|
24
24
|
return 1;
|
|
25
25
|
}
|
|
26
26
|
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
27
|
-
if (platform === '
|
|
27
|
+
if (platform === 'web') {
|
|
28
|
+
(0, output_js_1.printError)('install-app is not supported on web. Use launch-app with a URL instead.', opts);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
else if (platform === 'ios' || platform === 'tvos') {
|
|
28
32
|
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'install', deviceId, appPath]);
|
|
29
33
|
if (!result.success) {
|
|
30
34
|
(0, output_js_1.printError)(`install-app failed: ${result.stderr}`, opts);
|
|
@@ -21,7 +21,11 @@ async function listApps(opts = {}, sessionName = 'default') {
|
|
|
21
21
|
}
|
|
22
22
|
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
23
23
|
let appIds;
|
|
24
|
-
if (platform === '
|
|
24
|
+
if (platform === 'web') {
|
|
25
|
+
(0, output_js_1.printError)('list-apps is not supported on web. Use foreground-app to get the current URL.', opts);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
else if (platform === 'ios' || platform === 'tvos') {
|
|
25
29
|
const result = await (0, runner_js_1.spawnCommand)('bash', [
|
|
26
30
|
'-c',
|
|
27
31
|
`xcrun simctl listapps ${deviceId} | plutil -convert json - -o -`,
|
|
@@ -1,14 +1,19 @@
|
|
|
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.discoverBootedDevices = discoverBootedDevices;
|
|
5
8
|
exports.discoverAvailableDevices = discoverAvailableDevices;
|
|
6
9
|
exports.listDevices = listDevices;
|
|
7
10
|
exports.HELP = ` list-devices List booted and available devices/simulators`;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
12
|
const runner_js_1 = require("../runner.js");
|
|
9
13
|
const output_js_1 = require("../output.js");
|
|
10
14
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
11
15
|
const client_js_1 = require("../daemon/client.js");
|
|
16
|
+
const protocol_js_1 = require("../daemon/protocol.js");
|
|
12
17
|
async function discoverBootedDevices() {
|
|
13
18
|
const devices = [];
|
|
14
19
|
// Try adb devices (Android)
|
|
@@ -58,9 +63,15 @@ async function discoverBootedDevices() {
|
|
|
58
63
|
const status = await (0, client_js_1.daemonStatus)(session);
|
|
59
64
|
if (status.running) {
|
|
60
65
|
const browser = (0, bootstrap_js_1.webBrowserName)(session);
|
|
61
|
-
const parts = session.split(':');
|
|
62
66
|
const label = browser.charAt(0).toUpperCase() + browser.slice(1);
|
|
63
|
-
|
|
67
|
+
let name;
|
|
68
|
+
try {
|
|
69
|
+
const saved = fs_1.default.readFileSync((0, protocol_js_1.nameFile)(session), 'utf-8').trim();
|
|
70
|
+
name = saved || label;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
name = label;
|
|
74
|
+
}
|
|
64
75
|
devices.push({
|
|
65
76
|
id: session,
|
|
66
77
|
name,
|
|
@@ -106,6 +117,19 @@ async function discoverAvailableDevices() {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
}
|
|
120
|
+
// Web: installed Playwright browsers that aren't currently running
|
|
121
|
+
const runningSessions = (0, client_js_1.listDaemonSessions)();
|
|
122
|
+
const runningBrowsers = new Set(runningSessions.filter((s) => s === 'web' || s.startsWith('web:')).map((s) => (0, bootstrap_js_1.webBrowserName)(s)));
|
|
123
|
+
for (const browser of ['chromium', 'firefox', 'webkit']) {
|
|
124
|
+
if ((0, bootstrap_js_1.isPlaywrightBrowserInstalled)(browser)) {
|
|
125
|
+
devices.push({
|
|
126
|
+
id: browser === 'chromium' ? 'web' : `web:${browser}`,
|
|
127
|
+
name: browser.charAt(0).toUpperCase() + browser.slice(1),
|
|
128
|
+
platform: 'web',
|
|
129
|
+
status: runningBrowsers.has(browser) ? 'installed' : 'available',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
109
133
|
return devices;
|
|
110
134
|
}
|
|
111
135
|
async function listDevices(opts) {
|
|
@@ -113,20 +137,12 @@ async function listDevices(opts) {
|
|
|
113
137
|
discoverBootedDevices(),
|
|
114
138
|
discoverAvailableDevices(),
|
|
115
139
|
]);
|
|
116
|
-
|
|
117
|
-
const webBrowsers = ['chromium', 'firefox', 'webkit'].filter((b) => (0, bootstrap_js_1.isPlaywrightBrowserInstalled)(b));
|
|
118
|
-
if (devices.length === 0 && availableDevices.length === 0 && webBrowsers.length === 0) {
|
|
140
|
+
if (devices.length === 0 && availableDevices.length === 0) {
|
|
119
141
|
(0, output_js_1.printError)('No devices found. Start an emulator or simulator first.', opts);
|
|
120
142
|
return 1;
|
|
121
143
|
}
|
|
122
144
|
if (opts.json) {
|
|
123
|
-
|
|
124
|
-
id: b === 'chromium' ? 'web' : `web:${b}`,
|
|
125
|
-
name: b.charAt(0).toUpperCase() + b.slice(1),
|
|
126
|
-
platform: 'web',
|
|
127
|
-
status: 'available',
|
|
128
|
-
}));
|
|
129
|
-
(0, output_js_1.printData)({ status: 'ok', devices, availableDevices: [...availableDevices, ...webDevices] }, opts);
|
|
145
|
+
(0, output_js_1.printData)({ status: 'ok', devices, availableDevices }, opts);
|
|
130
146
|
}
|
|
131
147
|
else {
|
|
132
148
|
if (devices.length > 0) {
|
|
@@ -148,14 +164,6 @@ async function listDevices(opts) {
|
|
|
148
164
|
else {
|
|
149
165
|
console.log('No available devices.');
|
|
150
166
|
}
|
|
151
|
-
if (webBrowsers.length > 0) {
|
|
152
|
-
console.log('');
|
|
153
|
-
console.log('Web browsers:');
|
|
154
|
-
for (const b of webBrowsers) {
|
|
155
|
-
const deviceId = b === 'chromium' ? 'web' : `web:${b}`;
|
|
156
|
-
console.log(` web available ${deviceId.padEnd(16)} ${b.charAt(0).toUpperCase() + b.slice(1)}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
167
|
}
|
|
160
168
|
return 0;
|
|
161
169
|
}
|
|
@@ -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.startDevice = startDevice;
|
|
@@ -6,12 +9,15 @@ exports.HELP = ` start-device
|
|
|
6
9
|
--platform <ios|android|tvos|web> Boot a simulator/emulator, or start the web driver (Playwright)
|
|
7
10
|
--os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
|
|
8
11
|
--avd <name> Android AVD name (default: first available)
|
|
9
|
-
--name <name> Set a custom name on the
|
|
12
|
+
--name <name> Set a custom name on the device after creation (iOS/tvOS/web)
|
|
10
13
|
--device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K"); creates if needed
|
|
11
14
|
--browser <chromium|firefox|webkit> Web only: which Playwright browser to launch (default: chromium)`;
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
16
|
const child_process_1 = require("child_process");
|
|
13
17
|
const runner_js_1 = require("../runner.js");
|
|
14
18
|
const client_js_1 = require("../daemon/client.js");
|
|
19
|
+
const protocol_js_1 = require("../daemon/protocol.js");
|
|
20
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
15
21
|
const output_js_1 = require("../output.js");
|
|
16
22
|
const utils_js_1 = require("../utils.js");
|
|
17
23
|
const IOS_BOOT_TIMEOUT_MS = 120000;
|
|
@@ -462,34 +468,50 @@ async function startAndroid(avdName, opts) {
|
|
|
462
468
|
(0, output_js_1.printSuccess)(`Emulator ready: ${target} (${deviceId})`, opts);
|
|
463
469
|
return 0;
|
|
464
470
|
}
|
|
465
|
-
function
|
|
471
|
+
function resolveWebBrowser(browserArg) {
|
|
466
472
|
const b = (browserArg ?? 'chromium').toLowerCase();
|
|
467
473
|
switch (b) {
|
|
468
474
|
case 'chromium':
|
|
469
|
-
return { session: 'web' };
|
|
470
475
|
case 'firefox':
|
|
471
|
-
return { session: 'web:firefox' };
|
|
472
476
|
case 'webkit':
|
|
473
|
-
return {
|
|
477
|
+
return { browser: b };
|
|
474
478
|
default:
|
|
475
479
|
return {
|
|
476
480
|
error: `Unknown web browser "${browserArg}". Use chromium, firefox, or webkit.`,
|
|
477
481
|
};
|
|
478
482
|
}
|
|
479
483
|
}
|
|
480
|
-
async function startWebDriver(opts, browser) {
|
|
481
|
-
const resolved =
|
|
484
|
+
async function startWebDriver(opts, browser, name) {
|
|
485
|
+
const resolved = resolveWebBrowser(browser);
|
|
482
486
|
if ('error' in resolved) {
|
|
483
487
|
(0, output_js_1.printError)(resolved.error, opts);
|
|
484
488
|
return 1;
|
|
485
489
|
}
|
|
486
|
-
|
|
490
|
+
// Check for an existing running session of this browser type
|
|
491
|
+
const existing = await (0, client_js_1.findRunningWebSession)(resolved.browser);
|
|
492
|
+
if (existing && !name) {
|
|
493
|
+
(0, output_js_1.printSuccess)(`Web driver already running: ${existing}`, opts);
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
// Generate a unique session ID for this instance
|
|
497
|
+
const session = (0, bootstrap_js_1.generateWebSessionId)(resolved.browser);
|
|
498
|
+
const ready = await (0, client_js_1.startDaemon)(session);
|
|
487
499
|
if (!ready) {
|
|
488
|
-
(0, output_js_1.printError)(`Web driver did not become ready for session ${
|
|
500
|
+
(0, output_js_1.printError)(`Web driver did not become ready for session ${session}. ` +
|
|
489
501
|
'Install a browser with `conductor install-web` if needed, then retry.', opts);
|
|
490
502
|
return 1;
|
|
491
503
|
}
|
|
492
|
-
(
|
|
504
|
+
if (name) {
|
|
505
|
+
try {
|
|
506
|
+
fs_1.default.writeFileSync((0, protocol_js_1.nameFile)(session), name, 'utf-8');
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
/* best-effort — display name won't persist but the session works */
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const label = resolved.browser.charAt(0).toUpperCase() + resolved.browser.slice(1);
|
|
513
|
+
const displayName = name ? `${name} (${label})` : label;
|
|
514
|
+
(0, output_js_1.printSuccess)(`Web driver ready: ${displayName} (${session})`, opts);
|
|
493
515
|
return 0;
|
|
494
516
|
}
|
|
495
517
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
@@ -506,7 +528,7 @@ async function startDevice(platform, opts, flags) {
|
|
|
506
528
|
case 'android':
|
|
507
529
|
return startAndroid(flags.avd, opts);
|
|
508
530
|
case 'web':
|
|
509
|
-
return startWebDriver(opts, flags.browser);
|
|
531
|
+
return startWebDriver(opts, flags.browser, flags.name);
|
|
510
532
|
default:
|
|
511
533
|
(0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, or web.`, opts);
|
|
512
534
|
return 1;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
4
|
+
exports.stopDevice = stopDevice;
|
|
5
|
+
exports.HELP = ` stop-device [<name-or-id>]
|
|
6
|
+
--platform <ios|tvos|android|web> Scope to a single platform
|
|
7
|
+
--all Stop all booted simulators / running emulators / web sessions`;
|
|
8
|
+
const runner_js_1 = require("../runner.js");
|
|
9
|
+
const output_js_1 = require("../output.js");
|
|
10
|
+
const client_js_1 = require("../daemon/client.js");
|
|
11
|
+
const list_devices_js_1 = require("./list-devices.js");
|
|
12
|
+
// ── iOS / tvOS ───────────────────────────────────────────────────────────────
|
|
13
|
+
async function shutdownSimulator(udid) {
|
|
14
|
+
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'shutdown', udid]);
|
|
15
|
+
if (!result.success && !result.stderr.includes('current state: Shutdown')) {
|
|
16
|
+
throw new Error(`Failed to shutdown simulator: ${result.stderr.trim()}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// ── Android ──────────────────────────────────────────────────────────────────
|
|
20
|
+
async function killEmulator(serial) {
|
|
21
|
+
const result = await (0, runner_js_1.spawnCommand)('adb', ['-s', serial, 'emu', 'kill']);
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
throw new Error(`Failed to kill emulator ${serial}: ${result.stderr.trim()}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
27
|
+
async function stopDevice(nameOrId, opts, flags) {
|
|
28
|
+
if (!nameOrId && !flags.all) {
|
|
29
|
+
(0, output_js_1.printError)('stop-device requires a device name/ID, or --all', opts);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
const platform = flags.platform?.toLowerCase();
|
|
33
|
+
const includeIOS = !platform || platform === 'ios';
|
|
34
|
+
const includeTvOS = !platform || platform === 'tvos';
|
|
35
|
+
const includeAndroid = !platform || platform === 'android';
|
|
36
|
+
const includeWeb = !platform || platform === 'web';
|
|
37
|
+
const stopped = [];
|
|
38
|
+
// ── --all mode ───────────────────────────────────────────────────────────
|
|
39
|
+
if (flags.all) {
|
|
40
|
+
const devices = await (0, list_devices_js_1.discoverBootedDevices)();
|
|
41
|
+
for (const d of devices) {
|
|
42
|
+
if (d.platform === 'ios' && !includeIOS)
|
|
43
|
+
continue;
|
|
44
|
+
if (d.platform === 'tvos' && !includeTvOS)
|
|
45
|
+
continue;
|
|
46
|
+
if (d.platform === 'android' && !includeAndroid)
|
|
47
|
+
continue;
|
|
48
|
+
if (d.platform === 'web' && !includeWeb)
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
if (d.platform === 'ios' || d.platform === 'tvos') {
|
|
52
|
+
await shutdownSimulator(d.id);
|
|
53
|
+
}
|
|
54
|
+
else if (d.platform === 'android') {
|
|
55
|
+
await killEmulator(d.id);
|
|
56
|
+
}
|
|
57
|
+
else if (d.platform === 'web') {
|
|
58
|
+
await (0, client_js_1.stopDaemon)(d.id);
|
|
59
|
+
}
|
|
60
|
+
stopped.push({ id: d.id, name: d.name, platform: d.platform });
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
(0, output_js_1.printError)(`Failed to stop ${d.name} (${d.id}): ${e instanceof Error ? e.message : String(e)}`, opts);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (stopped.length === 0) {
|
|
67
|
+
(0, output_js_1.printError)('No running devices to stop.', opts);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
if (opts.json) {
|
|
71
|
+
(0, output_js_1.printData)({ status: 'ok', stopped }, opts);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
for (const d of stopped) {
|
|
75
|
+
(0, output_js_1.printSuccess)(`Stopped ${d.platform} device: ${d.name} (${d.id})`, opts);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
// ── Single device mode ─────────────────────────────────────────────────
|
|
81
|
+
const devices = await (0, list_devices_js_1.discoverBootedDevices)();
|
|
82
|
+
const match = devices.find((d) => {
|
|
83
|
+
if (platform && d.platform !== platform)
|
|
84
|
+
return false;
|
|
85
|
+
return d.id === nameOrId || d.name === nameOrId;
|
|
86
|
+
});
|
|
87
|
+
if (!match) {
|
|
88
|
+
(0, output_js_1.printError)(`No running device found matching "${nameOrId}".`, opts);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
if (match.platform === 'ios' || match.platform === 'tvos') {
|
|
93
|
+
await shutdownSimulator(match.id);
|
|
94
|
+
}
|
|
95
|
+
else if (match.platform === 'android') {
|
|
96
|
+
await killEmulator(match.id);
|
|
97
|
+
}
|
|
98
|
+
else if (match.platform === 'web') {
|
|
99
|
+
await (0, client_js_1.stopDaemon)(match.id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
if (opts.json) {
|
|
107
|
+
(0, output_js_1.printData)({ status: 'ok', stopped: [{ id: match.id, name: match.name, platform: match.platform }] }, opts);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
(0, output_js_1.printSuccess)(`Stopped ${match.platform} device: ${match.name} (${match.id})`, opts);
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
package/dist/daemon/client.js
CHANGED
|
@@ -141,7 +141,12 @@ async function stopDaemon(sessionName = 'default') {
|
|
|
141
141
|
}
|
|
142
142
|
// Clean up the daemon directory regardless — removes stale dirs from crashed daemons
|
|
143
143
|
const dir = path_1.default.join(os_1.default.homedir(), '.conductor', 'daemons', sessionName);
|
|
144
|
-
for (const file of [
|
|
144
|
+
for (const file of [
|
|
145
|
+
(0, protocol_js_1.socketPath)(sessionName),
|
|
146
|
+
(0, protocol_js_1.pidFile)(sessionName),
|
|
147
|
+
(0, protocol_js_1.logFile)(sessionName),
|
|
148
|
+
(0, protocol_js_1.nameFile)(sessionName),
|
|
149
|
+
]) {
|
|
145
150
|
try {
|
|
146
151
|
fs_1.default.unlinkSync(file);
|
|
147
152
|
}
|
package/dist/daemon/protocol.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.IDLE_TIMEOUT_MS = void 0;
|
|
|
7
7
|
exports.socketPath = socketPath;
|
|
8
8
|
exports.pidFile = pidFile;
|
|
9
9
|
exports.logFile = logFile;
|
|
10
|
+
exports.nameFile = nameFile;
|
|
10
11
|
const os_1 = __importDefault(require("os"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
12
13
|
const DIR = path_1.default.join(os_1.default.homedir(), '.conductor');
|
|
@@ -22,4 +23,7 @@ function pidFile(sessionName = 'default') {
|
|
|
22
23
|
function logFile(sessionName = 'default') {
|
|
23
24
|
return path_1.default.join(daemonDir(sessionName), 'daemon.log');
|
|
24
25
|
}
|
|
26
|
+
function nameFile(sessionName = 'default') {
|
|
27
|
+
return path_1.default.join(daemonDir(sessionName), 'name');
|
|
28
|
+
}
|
|
25
29
|
exports.IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
package/dist/daemon/server.js
CHANGED
|
@@ -41,6 +41,26 @@ const cdpUrl = process.env.CONDUCTOR_CDP_URL || undefined;
|
|
|
41
41
|
* URL heuristics.
|
|
42
42
|
*/
|
|
43
43
|
const cdpTargetId = process.env.CONDUCTOR_CDP_TARGET_ID || undefined;
|
|
44
|
+
/**
|
|
45
|
+
* PID of the process that should be considered the daemon's "owner". When set,
|
|
46
|
+
* the daemon polls for this process's existence and shuts down cleanly when it
|
|
47
|
+
* disappears. This prevents orphaned daemons (and their Playwright browsers)
|
|
48
|
+
* from piling up after the host app crashes or quits without calling
|
|
49
|
+
* `daemon-stop`.
|
|
50
|
+
*
|
|
51
|
+
* The daemon runs detached, so `process.ppid` becomes 1 after the parent exits
|
|
52
|
+
* and is useless for this purpose. The owner must be passed explicitly by the
|
|
53
|
+
* host app via the env when it invokes `conductor daemon-start` (or whatever
|
|
54
|
+
* code path ultimately triggers the daemon spawn).
|
|
55
|
+
*/
|
|
56
|
+
const parentPid = (() => {
|
|
57
|
+
const raw = process.env.CONDUCTOR_PARENT_PID;
|
|
58
|
+
if (!raw)
|
|
59
|
+
return undefined;
|
|
60
|
+
const n = parseInt(raw, 10);
|
|
61
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
62
|
+
})();
|
|
63
|
+
const PARENT_POLL_INTERVAL_MS = 10000;
|
|
44
64
|
const SOCKET_PATH = (0, protocol_js_1.socketPath)(sessionName);
|
|
45
65
|
const PID_FILE = (0, protocol_js_1.pidFile)(sessionName);
|
|
46
66
|
const LOG_FILE = (0, protocol_js_1.logFile)(sessionName);
|
|
@@ -60,6 +80,7 @@ let logCollector = null;
|
|
|
60
80
|
const DRIVER_HEALTH_INTERVAL_MS = 10000; // Check driver health every 10s
|
|
61
81
|
let _restartInProgress = false;
|
|
62
82
|
let _driverStarted = false;
|
|
83
|
+
let _driverStartError = null;
|
|
63
84
|
async function ensureDriverRunning() {
|
|
64
85
|
if (_restartInProgress || !_driverStarted)
|
|
65
86
|
return;
|
|
@@ -122,6 +143,7 @@ async function main() {
|
|
|
122
143
|
}
|
|
123
144
|
let idleTimer;
|
|
124
145
|
let healthTimer;
|
|
146
|
+
let parentWatchTimer;
|
|
125
147
|
const idleTimeoutMs = Number(process.env.CONDUCTOR_IDLE_TIMEOUT_MS) || protocol_js_1.IDLE_TIMEOUT_MS;
|
|
126
148
|
function resetIdleTimer() {
|
|
127
149
|
if (idleTimer)
|
|
@@ -134,6 +156,10 @@ async function main() {
|
|
|
134
156
|
async function cleanup() {
|
|
135
157
|
if (healthTimer)
|
|
136
158
|
clearInterval(healthTimer);
|
|
159
|
+
if (parentWatchTimer)
|
|
160
|
+
clearInterval(parentWatchTimer);
|
|
161
|
+
if (idleTimer)
|
|
162
|
+
clearTimeout(idleTimer);
|
|
137
163
|
if (logCollector) {
|
|
138
164
|
logCollector.stop();
|
|
139
165
|
logCollector = null;
|
|
@@ -214,6 +240,30 @@ async function main() {
|
|
|
214
240
|
}, DRIVER_HEALTH_INTERVAL_MS);
|
|
215
241
|
healthTimer.unref(); // Don't keep the process alive just for health checks
|
|
216
242
|
}
|
|
243
|
+
// If the host app told us who it is, shut down when it disappears. This is
|
|
244
|
+
// the primary defence against orphaned daemons + headless Chromiums when the
|
|
245
|
+
// host app crashes or force-quits without calling daemon-stop.
|
|
246
|
+
if (parentPid !== undefined) {
|
|
247
|
+
dlog(`Watching parent pid ${parentPid}`);
|
|
248
|
+
let shuttingDown = false;
|
|
249
|
+
parentWatchTimer = setInterval(() => {
|
|
250
|
+
if (shuttingDown)
|
|
251
|
+
return;
|
|
252
|
+
try {
|
|
253
|
+
process.kill(parentPid, 0);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
const code = err.code;
|
|
257
|
+
if (code === 'ESRCH') {
|
|
258
|
+
shuttingDown = true;
|
|
259
|
+
dlog(`Parent pid ${parentPid} exited — shutting down`);
|
|
260
|
+
cleanup().then(() => process.exit(0));
|
|
261
|
+
}
|
|
262
|
+
// EPERM means the process exists but we can't signal it — still alive.
|
|
263
|
+
}
|
|
264
|
+
}, PARENT_POLL_INTERVAL_MS);
|
|
265
|
+
parentWatchTimer.unref();
|
|
266
|
+
}
|
|
217
267
|
// ── HTTP server on Unix socket ─────────────────────────────────────────────
|
|
218
268
|
// Replaces the old raw-TCP accept-and-close with a proper HTTP server so we
|
|
219
269
|
// can serve /status (aliveness) and /logs (buffered log entries).
|
|
@@ -235,6 +285,9 @@ async function main() {
|
|
|
235
285
|
driverPort,
|
|
236
286
|
cdpUrl: cdpUrl ?? null,
|
|
237
287
|
cdpTargetId: cdpTargetId ?? null,
|
|
288
|
+
chromiumCdpPort: driverPlatform === 'web' ? (0, web_server_js_1.getCdpPort)() : null,
|
|
289
|
+
pageTargetId: driverPlatform === 'web' ? (0, web_server_js_1.getPageTargetId)() : null,
|
|
290
|
+
driverStartError: _driverStartError,
|
|
238
291
|
});
|
|
239
292
|
return;
|
|
240
293
|
}
|
|
@@ -325,7 +378,8 @@ async function main() {
|
|
|
325
378
|
dlog(`Driver started successfully`);
|
|
326
379
|
}
|
|
327
380
|
catch (err) {
|
|
328
|
-
|
|
381
|
+
_driverStartError = err instanceof Error ? err.message : String(err);
|
|
382
|
+
dlog(`Driver startup error: ${_driverStartError}`);
|
|
329
383
|
}
|
|
330
384
|
}
|
|
331
385
|
// Start collecting logs once the driver is (or was already) running.
|
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.parseAriaSnapshot = parseAriaSnapshot;
|
|
7
7
|
exports.startWebServer = startWebServer;
|
|
8
8
|
exports.stopWebServer = stopWebServer;
|
|
9
|
+
exports.getCdpPort = getCdpPort;
|
|
10
|
+
exports.getPageTargetId = getPageTargetId;
|
|
9
11
|
/**
|
|
10
12
|
* Daemon-embedded HTTP server wrapping Playwright for web browser control.
|
|
11
13
|
*
|
|
@@ -17,6 +19,7 @@ exports.stopWebServer = stopWebServer;
|
|
|
17
19
|
* happen here so the CLI remains a thin HTTP client.
|
|
18
20
|
*/
|
|
19
21
|
const http_1 = __importDefault(require("http"));
|
|
22
|
+
const net_1 = require("net");
|
|
20
23
|
const url_1 = __importDefault(require("url"));
|
|
21
24
|
const playwright_core_1 = require("playwright-core");
|
|
22
25
|
const MAX_CONSOLE_BUFFER = 1000;
|
|
@@ -432,6 +435,34 @@ let _server = null;
|
|
|
432
435
|
* — we only disconnect.
|
|
433
436
|
*/
|
|
434
437
|
let _cdpMode = false;
|
|
438
|
+
let _cdpPort = 0;
|
|
439
|
+
let _pageTargetId = null;
|
|
440
|
+
/** Find a free TCP port by briefly binding to port 0. */
|
|
441
|
+
function findFreePort() {
|
|
442
|
+
return new Promise((resolve, reject) => {
|
|
443
|
+
const srv = (0, net_1.createServer)();
|
|
444
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
445
|
+
const addr = srv.address();
|
|
446
|
+
const port = typeof addr === 'object' ? (addr?.port ?? 0) : 0;
|
|
447
|
+
srv.close(() => resolve(port));
|
|
448
|
+
});
|
|
449
|
+
srv.on('error', reject);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/** Resolve the CDP target ID of the current page via a temporary CDP session. */
|
|
453
|
+
async function resolvePageTargetId() {
|
|
454
|
+
if (!_context || !_page)
|
|
455
|
+
return null;
|
|
456
|
+
try {
|
|
457
|
+
const cdpSession = await _context.newCDPSession(_page);
|
|
458
|
+
const info = (await cdpSession.send('Target.getTargetInfo'));
|
|
459
|
+
await cdpSession.detach();
|
|
460
|
+
return info.targetInfo?.targetId ?? null;
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
435
466
|
async function startWebServer(port, browserName = 'chromium', dlog = () => { }, cdpUrl, cdpTargetId) {
|
|
436
467
|
if (cdpUrl) {
|
|
437
468
|
// ── CDP mode: attach to an existing browser (e.g. Electron webview) ───
|
|
@@ -514,17 +545,25 @@ async function startWebServer(port, browserName = 'chromium', dlog = () => { },
|
|
|
514
545
|
else {
|
|
515
546
|
// ── Standalone mode: launch a fresh browser ───────────────────────────
|
|
516
547
|
const browserType = browserName === 'firefox' ? playwright_core_1.firefox : browserName === 'webkit' ? playwright_core_1.webkit : playwright_core_1.chromium;
|
|
517
|
-
|
|
548
|
+
if (browserName === 'chromium') {
|
|
549
|
+
_cdpPort = await findFreePort();
|
|
550
|
+
dlog(`Allocated CDP port ${_cdpPort} for remote debugging`);
|
|
551
|
+
}
|
|
552
|
+
const headless = process.env.CONDUCTOR_HEADLESS === '1' || process.env.CONDUCTOR_HEADLESS === 'true';
|
|
553
|
+
dlog(`Launching ${browserName} browser (headless=${headless})...`);
|
|
518
554
|
_browser = await browserType.launch({
|
|
519
|
-
headless
|
|
520
|
-
args: browserName === 'chromium'
|
|
555
|
+
headless,
|
|
556
|
+
args: browserName === 'chromium'
|
|
557
|
+
? [`--remote-debugging-port=${_cdpPort}`, '--disable-search-engine-choice-screen']
|
|
558
|
+
: undefined,
|
|
521
559
|
});
|
|
522
560
|
_context = await _browser.newContext({
|
|
523
561
|
viewport: DEFAULT_VIEWPORT,
|
|
524
562
|
});
|
|
525
563
|
_page = await _context.newPage();
|
|
526
564
|
attachConsoleListeners(_page);
|
|
527
|
-
|
|
565
|
+
_pageTargetId = await resolvePageTargetId();
|
|
566
|
+
dlog(`Browser ready, page created (targetId=${_pageTargetId ?? 'unknown'})`);
|
|
528
567
|
_cdpMode = false;
|
|
529
568
|
}
|
|
530
569
|
_server = http_1.default.createServer(async (req, res) => {
|
|
@@ -582,6 +621,32 @@ async function stopWebServer() {
|
|
|
582
621
|
}
|
|
583
622
|
}
|
|
584
623
|
_cdpMode = false;
|
|
624
|
+
_cdpPort = 0;
|
|
625
|
+
_pageTargetId = null;
|
|
626
|
+
}
|
|
627
|
+
/** Get the CDP remote debugging port (0 if not running or non-Chromium). */
|
|
628
|
+
function getCdpPort() {
|
|
629
|
+
return _cdpPort;
|
|
630
|
+
}
|
|
631
|
+
/** Get the CDP target ID for the current page (null if unavailable). */
|
|
632
|
+
function getPageTargetId() {
|
|
633
|
+
return _pageTargetId;
|
|
634
|
+
}
|
|
635
|
+
/** Query the page's navigation history via CDP to determine back/forward state. */
|
|
636
|
+
async function getNavState(page) {
|
|
637
|
+
try {
|
|
638
|
+
const session = await page.context().newCDPSession(page);
|
|
639
|
+
const { currentIndex, entries } = await session.send('Page.getNavigationHistory');
|
|
640
|
+
await session.detach().catch(() => { });
|
|
641
|
+
return {
|
|
642
|
+
url: page.url(),
|
|
643
|
+
canGoBack: currentIndex > 0,
|
|
644
|
+
canGoForward: currentIndex < entries.length - 1,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return { url: page.url(), canGoBack: false, canGoForward: false };
|
|
649
|
+
}
|
|
585
650
|
}
|
|
586
651
|
/** Playwright / CDP errors when the tab, context, or session died but our JS refs still exist. */
|
|
587
652
|
function isClosedLikeError(err) {
|
|
@@ -858,26 +923,29 @@ async function handleRequest(req, res, dlog) {
|
|
|
858
923
|
return;
|
|
859
924
|
}
|
|
860
925
|
await gotoWithRecovery(targetUrl, dlog);
|
|
861
|
-
|
|
926
|
+
const navState = await getNavState(await getPage(dlog));
|
|
927
|
+
jsonResponse(res, { ok: true, ...navState });
|
|
862
928
|
return;
|
|
863
929
|
}
|
|
864
930
|
case '/goBack': {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
jsonResponse(res, { ok: true });
|
|
931
|
+
const p = await getPage(dlog);
|
|
932
|
+
await p.goBack({ waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => { });
|
|
933
|
+
const navState = await getNavState(p);
|
|
934
|
+
jsonResponse(res, { ok: true, ...navState });
|
|
869
935
|
return;
|
|
870
936
|
}
|
|
871
937
|
case '/goForward': {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
jsonResponse(res, { ok: true });
|
|
938
|
+
const p = await getPage(dlog);
|
|
939
|
+
await p.goForward({ waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => { });
|
|
940
|
+
const navState = await getNavState(p);
|
|
941
|
+
jsonResponse(res, { ok: true, ...navState });
|
|
876
942
|
return;
|
|
877
943
|
}
|
|
878
944
|
case '/reload': {
|
|
879
|
-
|
|
880
|
-
|
|
945
|
+
const p = await getPage(dlog);
|
|
946
|
+
await p.reload({ waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
947
|
+
const navState = await getNavState(p);
|
|
948
|
+
jsonResponse(res, { ok: true, ...navState });
|
|
881
949
|
return;
|
|
882
950
|
}
|
|
883
951
|
case '/clearCookies': {
|
|
@@ -920,6 +988,53 @@ async function handleRequest(req, res, dlog) {
|
|
|
920
988
|
jsonResponse(res, { ok: true });
|
|
921
989
|
return;
|
|
922
990
|
}
|
|
991
|
+
case '/setViewport': {
|
|
992
|
+
if (_cdpMode) {
|
|
993
|
+
jsonResponse(res, { error: 'setViewport not supported in CDP mode' }, 400);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (!_browser) {
|
|
997
|
+
jsonResponse(res, { error: 'No browser running' }, 400);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const vpWidth = body['width'] ?? DEFAULT_VIEWPORT.width;
|
|
1001
|
+
const vpHeight = body['height'] ?? DEFAULT_VIEWPORT.height;
|
|
1002
|
+
const vpUserAgent = body['userAgent'];
|
|
1003
|
+
const vpIsMobile = body['isMobile'] ?? false;
|
|
1004
|
+
const vpScaleFactor = body['deviceScaleFactor'] ?? (vpIsMobile ? 2 : 1);
|
|
1005
|
+
const vpColorScheme = body['colorScheme'];
|
|
1006
|
+
const savedUrl = _page ? _page.url() : '';
|
|
1007
|
+
if (_page)
|
|
1008
|
+
await _page.close().catch(() => { });
|
|
1009
|
+
if (_context)
|
|
1010
|
+
await _context.close().catch(() => { });
|
|
1011
|
+
_context = await _browser.newContext({
|
|
1012
|
+
viewport: { width: vpWidth, height: vpHeight },
|
|
1013
|
+
userAgent: vpUserAgent,
|
|
1014
|
+
deviceScaleFactor: vpScaleFactor,
|
|
1015
|
+
isMobile: vpIsMobile,
|
|
1016
|
+
hasTouch: vpIsMobile,
|
|
1017
|
+
colorScheme: vpColorScheme,
|
|
1018
|
+
});
|
|
1019
|
+
_page = await _context.newPage();
|
|
1020
|
+
attachConsoleListeners(_page);
|
|
1021
|
+
_pageTargetId = await resolvePageTargetId();
|
|
1022
|
+
if (savedUrl && savedUrl !== 'about:blank') {
|
|
1023
|
+
await gotoWithRecovery(savedUrl, dlog);
|
|
1024
|
+
}
|
|
1025
|
+
jsonResponse(res, { ok: true, cdpTargetId: _pageTargetId });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
case '/setColorScheme': {
|
|
1029
|
+
const scheme = body['colorScheme'];
|
|
1030
|
+
if (scheme !== 'dark' && scheme !== 'light') {
|
|
1031
|
+
jsonResponse(res, { error: 'colorScheme must be "dark" or "light"' }, 400);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
await (await getPage(dlog)).emulateMedia({ colorScheme: scheme });
|
|
1035
|
+
jsonResponse(res, { ok: true });
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
923
1038
|
case '/shutdown': {
|
|
924
1039
|
jsonResponse(res, { ok: true });
|
|
925
1040
|
// Graceful shutdown after response is sent
|
|
@@ -608,7 +608,12 @@ async function stopWebDriver(port) {
|
|
|
608
608
|
*/
|
|
609
609
|
async function uninstallDriver(deviceId, platform) {
|
|
610
610
|
(0, verbose_js_1.log)(`uninstallDriver: removing ${platform} driver from ${deviceId}`);
|
|
611
|
-
if (platform === '
|
|
611
|
+
if (platform === 'web') {
|
|
612
|
+
// Web has no persistent driver to uninstall — the browser is managed by the daemon.
|
|
613
|
+
// Stopping the web server is handled by the daemon cleanup.
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
else if (platform === 'ios') {
|
|
612
617
|
await spawnAndWait('xcrun', [
|
|
613
618
|
'simctl',
|
|
614
619
|
'uninstall',
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
|
|
|
42
42
|
const set_location_js_1 = require("./commands/set-location.js");
|
|
43
43
|
const set_orientation_js_1 = require("./commands/set-orientation.js");
|
|
44
44
|
const start_device_js_1 = require("./commands/start-device.js");
|
|
45
|
+
const stop_device_js_1 = require("./commands/stop-device.js");
|
|
45
46
|
const delete_device_js_1 = require("./commands/delete-device.js");
|
|
46
47
|
const logs_js_1 = require("./commands/logs.js");
|
|
47
48
|
const device_picker_js_1 = require("./device-picker.js");
|
|
@@ -51,6 +52,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
51
52
|
const path_1 = __importDefault(require("path"));
|
|
52
53
|
const COMMAND_HELP = {
|
|
53
54
|
'start-device': start_device_js_1.HELP,
|
|
55
|
+
'stop-device': stop_device_js_1.HELP,
|
|
54
56
|
'delete-device': delete_device_js_1.HELP,
|
|
55
57
|
'list-devices': list_devices_js_1.HELP,
|
|
56
58
|
'foreground-app': foreground_app_js_1.HELP,
|
|
@@ -187,6 +189,7 @@ async function main() {
|
|
|
187
189
|
const NO_DEVICE_COMMANDS = new Set([
|
|
188
190
|
'list-devices',
|
|
189
191
|
'start-device',
|
|
192
|
+
'stop-device',
|
|
190
193
|
'delete-device',
|
|
191
194
|
'cheat-sheet',
|
|
192
195
|
'install-plugin',
|
|
@@ -244,6 +247,12 @@ async function main() {
|
|
|
244
247
|
case 'list-devices':
|
|
245
248
|
exitCode = await (0, list_devices_js_1.listDevices)(opts);
|
|
246
249
|
break;
|
|
250
|
+
case 'stop-device':
|
|
251
|
+
exitCode = await (0, stop_device_js_1.stopDevice)(rest[0], opts, {
|
|
252
|
+
platform: argv['platform'],
|
|
253
|
+
all: argv['all'],
|
|
254
|
+
});
|
|
255
|
+
break;
|
|
247
256
|
case 'delete-device':
|
|
248
257
|
exitCode = await (0, delete_device_js_1.deleteDevice)(rest[0], opts, {
|
|
249
258
|
platform: argv['platform'],
|
package/dist/runner.js
CHANGED
|
@@ -60,6 +60,17 @@ async function detectFirstDevice() {
|
|
|
60
60
|
/* ignore */
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
// Web: check for running daemon web sessions
|
|
64
|
+
for (const session of (0, client_js_1.listDaemonSessions)()) {
|
|
65
|
+
if (!(session === 'web' || session.startsWith('web:')))
|
|
66
|
+
continue;
|
|
67
|
+
const status = await (0, client_js_1.daemonStatus)(session);
|
|
68
|
+
if (status.running) {
|
|
69
|
+
(0, verbose_js_1.log)(`detectFirstDevice: found running web session "${session}"`);
|
|
70
|
+
_cachedDeviceId = session;
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
_cachedDeviceId = null;
|
|
64
75
|
return undefined;
|
|
65
76
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED