@myerscarpenter/quest-dev 1.1.0 → 1.2.1
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/build/commands/battery.d.ts +9 -0
- package/build/commands/battery.d.ts.map +1 -0
- package/build/commands/battery.js +31 -0
- package/build/commands/battery.js.map +1 -0
- package/build/commands/logcat.d.ts.map +1 -1
- package/build/commands/logcat.js +8 -6
- package/build/commands/logcat.js.map +1 -1
- package/build/commands/open.d.ts +1 -1
- package/build/commands/open.d.ts.map +1 -1
- package/build/commands/open.js +14 -14
- package/build/commands/open.js.map +1 -1
- package/build/commands/screenshot.d.ts +1 -1
- package/build/commands/screenshot.d.ts.map +1 -1
- package/build/commands/screenshot.js +55 -9
- package/build/commands/screenshot.js.map +1 -1
- package/build/commands/stay-awake.d.ts +14 -0
- package/build/commands/stay-awake.d.ts.map +1 -0
- package/build/commands/stay-awake.js +234 -0
- package/build/commands/stay-awake.js.map +1 -0
- package/build/index.js +49 -5
- package/build/index.js.map +1 -1
- package/build/utils/adb.d.ts +12 -7
- package/build/utils/adb.d.ts.map +1 -1
- package/build/utils/adb.js +138 -30
- package/build/utils/adb.js.map +1 -1
- package/build/utils/exec.d.ts.map +1 -1
- package/build/utils/exec.js +0 -2
- package/build/utils/exec.js.map +1 -1
- package/build/utils/filename.d.ts +9 -0
- package/build/utils/filename.d.ts.map +1 -0
- package/build/utils/filename.js +17 -0
- package/build/utils/filename.js.map +1 -0
- package/build/utils/filename.test.d.ts +5 -0
- package/build/utils/filename.test.d.ts.map +1 -0
- package/build/utils/filename.test.js +40 -0
- package/build/utils/filename.test.js.map +1 -0
- package/package.json +2 -1
- package/src/commands/battery.ts +34 -0
- package/src/commands/logcat.ts +7 -5
- package/src/commands/open.ts +18 -14
- package/src/commands/screenshot.ts +61 -9
- package/src/commands/stay-awake.ts +254 -0
- package/src/index.ts +77 -9
- package/src/utils/adb.ts +148 -30
- package/src/utils/exec.ts +0 -2
- package/src/utils/filename.test.ts +55 -0
- package/src/utils/filename.ts +18 -0
- package/tests/adb.test.ts +2 -2
- package/tests/exec.test.ts +3 -3
package/src/utils/adb.ts
CHANGED
|
@@ -8,6 +8,63 @@ import { execCommand, execCommandFull } from './exec.js';
|
|
|
8
8
|
|
|
9
9
|
const CDP_PORT = 9223; // Chrome DevTools Protocol port (Quest browser default)
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Get browser process PID
|
|
13
|
+
*/
|
|
14
|
+
async function getBrowserPID(packageName: string): Promise<number | null> {
|
|
15
|
+
try {
|
|
16
|
+
const result = await execCommandFull('adb', ['shell', `ps | grep ${packageName}`]);
|
|
17
|
+
if (!result.stdout) return null;
|
|
18
|
+
|
|
19
|
+
// Parse ps output: USER PID PPID ... NAME
|
|
20
|
+
const lines = result.stdout.trim().split('\n');
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (line.includes('grep')) continue; // Skip grep itself
|
|
23
|
+
const parts = line.trim().split(/\s+/);
|
|
24
|
+
if (parts.length >= 2) {
|
|
25
|
+
return parseInt(parts[1], 10); // PID is second column
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect CDP socket for a browser
|
|
36
|
+
* Returns socket name (e.g., "chrome_devtools_remote_12345")
|
|
37
|
+
*/
|
|
38
|
+
async function detectCDPSocket(packageName: string): Promise<string> {
|
|
39
|
+
const pid = await getBrowserPID(packageName);
|
|
40
|
+
|
|
41
|
+
if (pid) {
|
|
42
|
+
// Try PID-based socket first
|
|
43
|
+
try {
|
|
44
|
+
const result = await execCommandFull('adb', [
|
|
45
|
+
'shell',
|
|
46
|
+
`cat /proc/net/unix | grep chrome_devtools_remote_${pid}`
|
|
47
|
+
]);
|
|
48
|
+
if (result.stdout.includes(`chrome_devtools_remote_${pid}`)) {
|
|
49
|
+
return `chrome_devtools_remote_${pid}`;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Fall through to default
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Default: generic socket (Quest Browser)
|
|
57
|
+
return 'chrome_devtools_remote';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get CDP port for a socket
|
|
62
|
+
* Generic socket uses 9223, PID-based uses 9222
|
|
63
|
+
*/
|
|
64
|
+
function getCDPPortForSocket(socket: string): number {
|
|
65
|
+
return socket === 'chrome_devtools_remote' ? 9223 : 9222;
|
|
66
|
+
}
|
|
67
|
+
|
|
11
68
|
/**
|
|
12
69
|
* Check if ADB is available on PATH
|
|
13
70
|
*/
|
|
@@ -115,8 +172,15 @@ export function isPortListening(port: number): Promise<boolean> {
|
|
|
115
172
|
/**
|
|
116
173
|
* Idempotently set up ADB port forwarding for a given port
|
|
117
174
|
*/
|
|
118
|
-
export async function ensurePortForwarding(
|
|
175
|
+
export async function ensurePortForwarding(
|
|
176
|
+
port: number,
|
|
177
|
+
browser: string = 'com.oculus.browser'
|
|
178
|
+
): Promise<void> {
|
|
119
179
|
try {
|
|
180
|
+
// Detect CDP socket and port for this browser
|
|
181
|
+
const cdpSocket = await detectCDPSocket(browser);
|
|
182
|
+
const cdpPort = getCDPPortForSocket(cdpSocket);
|
|
183
|
+
|
|
120
184
|
// Check reverse forwarding (Quest -> Host for dev server)
|
|
121
185
|
const reverseList = await execCommand('adb', ['reverse', '--list']);
|
|
122
186
|
const reverseExists = reverseList.includes(`tcp:${port}`);
|
|
@@ -131,27 +195,27 @@ export async function ensurePortForwarding(port: number): Promise<void> {
|
|
|
131
195
|
// Check forward forwarding (Host -> Quest for CDP)
|
|
132
196
|
// First check if ADB already has this forwarding set up
|
|
133
197
|
const forwardList = await execCommand('adb', ['forward', '--list']);
|
|
134
|
-
const forwardExists = forwardList.includes(`tcp:${
|
|
198
|
+
const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
|
|
135
199
|
|
|
136
200
|
if (forwardExists) {
|
|
137
|
-
console.log(`CDP port ${
|
|
201
|
+
console.log(`CDP port ${cdpPort} forwarding already set up`);
|
|
138
202
|
} else {
|
|
139
203
|
// Check if something else is using the port
|
|
140
|
-
const cdpPortListening = await isPortListening(
|
|
204
|
+
const cdpPortListening = await isPortListening(cdpPort);
|
|
141
205
|
if (cdpPortListening) {
|
|
142
|
-
console.error(`Error: Port ${
|
|
206
|
+
console.error(`Error: Port ${cdpPort} is already in use by another process`);
|
|
143
207
|
console.error('');
|
|
144
|
-
console.error(
|
|
208
|
+
console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
|
|
145
209
|
console.error('Please stop the process using this port and try again.');
|
|
146
210
|
console.error('');
|
|
147
211
|
console.error('To find what is using the port:');
|
|
148
|
-
console.error(` lsof -i :${
|
|
212
|
+
console.error(` lsof -i :${cdpPort}`);
|
|
149
213
|
console.error('');
|
|
150
214
|
process.exit(1);
|
|
151
215
|
}
|
|
152
216
|
|
|
153
|
-
await execCommand('adb', ['forward', `tcp:${
|
|
154
|
-
console.log(`ADB forward port forwarding set up: Host:${
|
|
217
|
+
await execCommand('adb', ['forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`]);
|
|
218
|
+
console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
|
|
155
219
|
}
|
|
156
220
|
} catch (error) {
|
|
157
221
|
console.error('Failed to set up port forwarding:', (error as Error).message);
|
|
@@ -160,22 +224,22 @@ export async function ensurePortForwarding(port: number): Promise<void> {
|
|
|
160
224
|
}
|
|
161
225
|
|
|
162
226
|
/**
|
|
163
|
-
* Check if
|
|
227
|
+
* Check if browser is running
|
|
164
228
|
*/
|
|
165
|
-
export async function isBrowserRunning(): Promise<boolean> {
|
|
229
|
+
export async function isBrowserRunning(browser: string = 'com.oculus.browser'): Promise<boolean> {
|
|
166
230
|
try {
|
|
167
|
-
const result = await execCommandFull('adb', ['shell',
|
|
168
|
-
return result.stdout.includes(
|
|
231
|
+
const result = await execCommandFull('adb', ['shell', `ps | grep ${browser}`]);
|
|
232
|
+
return result.stdout.includes(browser);
|
|
169
233
|
} catch (error) {
|
|
170
234
|
return false;
|
|
171
235
|
}
|
|
172
236
|
}
|
|
173
237
|
|
|
174
238
|
/**
|
|
175
|
-
* Launch
|
|
239
|
+
* Launch browser with a URL using am start
|
|
176
240
|
*/
|
|
177
|
-
export async function launchBrowser(url: string): Promise<boolean> {
|
|
178
|
-
console.log('Launching
|
|
241
|
+
export async function launchBrowser(url: string, browser: string = 'com.oculus.browser'): Promise<boolean> {
|
|
242
|
+
console.log('Launching browser...');
|
|
179
243
|
try {
|
|
180
244
|
await execCommand('adb', [
|
|
181
245
|
'shell',
|
|
@@ -185,12 +249,12 @@ export async function launchBrowser(url: string): Promise<boolean> {
|
|
|
185
249
|
'android.intent.action.VIEW',
|
|
186
250
|
'-d',
|
|
187
251
|
url,
|
|
188
|
-
|
|
252
|
+
browser
|
|
189
253
|
]);
|
|
190
|
-
console.log(`
|
|
254
|
+
console.log(`Browser launched with URL: ${url}`);
|
|
191
255
|
return true;
|
|
192
256
|
} catch (error) {
|
|
193
|
-
console.error('Failed to launch
|
|
257
|
+
console.error('Failed to launch browser:', (error as Error).message);
|
|
194
258
|
return false;
|
|
195
259
|
}
|
|
196
260
|
}
|
|
@@ -198,38 +262,45 @@ export async function launchBrowser(url: string): Promise<boolean> {
|
|
|
198
262
|
/**
|
|
199
263
|
* Get CDP port
|
|
200
264
|
*/
|
|
201
|
-
export function getCDPPort(): number {
|
|
202
|
-
|
|
265
|
+
export async function getCDPPort(browser: string = 'com.oculus.browser'): Promise<number> {
|
|
266
|
+
const cdpSocket = await detectCDPSocket(browser);
|
|
267
|
+
return getCDPPortForSocket(cdpSocket);
|
|
203
268
|
}
|
|
204
269
|
|
|
205
270
|
/**
|
|
206
271
|
* Set up only CDP forwarding (for external URLs that don't need reverse forwarding)
|
|
207
272
|
*/
|
|
208
|
-
export async function ensureCDPForwarding(
|
|
273
|
+
export async function ensureCDPForwarding(
|
|
274
|
+
browser: string = 'com.oculus.browser'
|
|
275
|
+
): Promise<void> {
|
|
209
276
|
try {
|
|
277
|
+
// Detect CDP socket and port for this browser
|
|
278
|
+
const cdpSocket = await detectCDPSocket(browser);
|
|
279
|
+
const cdpPort = getCDPPortForSocket(cdpSocket);
|
|
280
|
+
|
|
210
281
|
// Check forward forwarding (Host -> Quest for CDP)
|
|
211
282
|
const forwardList = await execCommand('adb', ['forward', '--list']);
|
|
212
|
-
const forwardExists = forwardList.includes(`tcp:${
|
|
283
|
+
const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
|
|
213
284
|
|
|
214
285
|
if (forwardExists) {
|
|
215
|
-
console.log(`CDP port ${
|
|
286
|
+
console.log(`CDP port ${cdpPort} forwarding already set up`);
|
|
216
287
|
} else {
|
|
217
288
|
// Check if something else is using the port
|
|
218
|
-
const cdpPortListening = await isPortListening(
|
|
289
|
+
const cdpPortListening = await isPortListening(cdpPort);
|
|
219
290
|
if (cdpPortListening) {
|
|
220
|
-
console.error(`Error: Port ${
|
|
291
|
+
console.error(`Error: Port ${cdpPort} is already in use by another process`);
|
|
221
292
|
console.error('');
|
|
222
|
-
console.error(
|
|
293
|
+
console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
|
|
223
294
|
console.error('Please stop the process using this port and try again.');
|
|
224
295
|
console.error('');
|
|
225
296
|
console.error('To find what is using the port:');
|
|
226
|
-
console.error(` lsof -i :${
|
|
297
|
+
console.error(` lsof -i :${cdpPort}`);
|
|
227
298
|
console.error('');
|
|
228
299
|
process.exit(1);
|
|
229
300
|
}
|
|
230
301
|
|
|
231
|
-
await execCommand('adb', ['forward', `tcp:${
|
|
232
|
-
console.log(`ADB forward port forwarding set up: Host:${
|
|
302
|
+
await execCommand('adb', ['forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`]);
|
|
303
|
+
console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
|
|
233
304
|
}
|
|
234
305
|
} catch (error) {
|
|
235
306
|
console.error('Failed to set up CDP forwarding:', (error as Error).message);
|
|
@@ -273,3 +344,50 @@ export async function checkQuestAwake(): Promise<void> {
|
|
|
273
344
|
process.exit(1);
|
|
274
345
|
}
|
|
275
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get Quest battery status
|
|
350
|
+
* Returns percentage and charging state in one line
|
|
351
|
+
*/
|
|
352
|
+
export async function getBatteryStatus(): Promise<string> {
|
|
353
|
+
const result = await execCommandFull('adb', ['shell', 'dumpsys', 'battery']);
|
|
354
|
+
|
|
355
|
+
if (result.code !== 0) {
|
|
356
|
+
throw new Error('Failed to get battery status');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Parse battery info
|
|
360
|
+
let level = 0;
|
|
361
|
+
let acPowered = false;
|
|
362
|
+
let usbPowered = false;
|
|
363
|
+
let maxChargingCurrent = 0;
|
|
364
|
+
|
|
365
|
+
const lines = result.stdout.split('\n');
|
|
366
|
+
for (const line of lines) {
|
|
367
|
+
const trimmed = line.trim();
|
|
368
|
+
if (trimmed.startsWith('level: ')) {
|
|
369
|
+
level = parseInt(trimmed.substring(7), 10);
|
|
370
|
+
} else if (trimmed.startsWith('AC powered: ')) {
|
|
371
|
+
acPowered = trimmed.substring(12) === 'true';
|
|
372
|
+
} else if (trimmed.startsWith('USB powered: ')) {
|
|
373
|
+
usbPowered = trimmed.substring(13) === 'true';
|
|
374
|
+
} else if (trimmed.startsWith('Max charging current: ')) {
|
|
375
|
+
maxChargingCurrent = parseInt(trimmed.substring(22), 10);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Determine charging state
|
|
380
|
+
let state: string;
|
|
381
|
+
if (acPowered || usbPowered) {
|
|
382
|
+
// Fast charging is typically > 2A (2000000 microamps)
|
|
383
|
+
if (maxChargingCurrent > 2000000) {
|
|
384
|
+
state = 'fast charging';
|
|
385
|
+
} else {
|
|
386
|
+
state = 'charging';
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
state = 'not charging';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return `${level}% ${state}`;
|
|
393
|
+
}
|
package/src/utils/exec.ts
CHANGED
|
@@ -17,7 +17,6 @@ export function execCommand(command: string, args: string[] = []): Promise<strin
|
|
|
17
17
|
return new Promise((resolve, reject) => {
|
|
18
18
|
const proc = spawn(command, args, {
|
|
19
19
|
stdio: 'pipe',
|
|
20
|
-
shell: true
|
|
21
20
|
});
|
|
22
21
|
|
|
23
22
|
let stdout = '';
|
|
@@ -54,7 +53,6 @@ export function execCommandFull(command: string, args: string[] = []): Promise<E
|
|
|
54
53
|
return new Promise((resolve) => {
|
|
55
54
|
const proc = spawn(command, args, {
|
|
56
55
|
stdio: 'pipe',
|
|
57
|
-
shell: true
|
|
58
56
|
});
|
|
59
57
|
|
|
60
58
|
let stdout = '';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for filename generation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { generateScreenshotFilename } from './filename.js';
|
|
7
|
+
|
|
8
|
+
describe('generateScreenshotFilename', () => {
|
|
9
|
+
it('formats UTC timestamp correctly', () => {
|
|
10
|
+
const date = new Date('2026-01-15T14:30:45Z');
|
|
11
|
+
expect(generateScreenshotFilename(date))
|
|
12
|
+
.toBe('screenshot-2026-01-15-14-30-45-Z.jpg');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('pads single digits with zeros', () => {
|
|
16
|
+
const date = new Date('2026-01-05T08:09:03Z');
|
|
17
|
+
expect(generateScreenshotFilename(date))
|
|
18
|
+
.toBe('screenshot-2026-01-05-08-09-03-Z.jpg');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('handles midnight correctly', () => {
|
|
22
|
+
const date = new Date('2026-12-31T00:00:00Z');
|
|
23
|
+
expect(generateScreenshotFilename(date))
|
|
24
|
+
.toBe('screenshot-2026-12-31-00-00-00-Z.jpg');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('generates filename with current time when no date provided', () => {
|
|
28
|
+
const filename = generateScreenshotFilename();
|
|
29
|
+
|
|
30
|
+
// Verify format is correct
|
|
31
|
+
expect(filename).toMatch(/^screenshot-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-Z\.jpg$/);
|
|
32
|
+
|
|
33
|
+
// Extract timestamp from filename
|
|
34
|
+
const match = filename.match(/^screenshot-(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})-(\d{2})-Z\.jpg$/);
|
|
35
|
+
expect(match).not.toBeNull();
|
|
36
|
+
|
|
37
|
+
if (match) {
|
|
38
|
+
const [, year, month, day, hours, minutes, seconds] = match;
|
|
39
|
+
const filenameDate = new Date(Date.UTC(
|
|
40
|
+
parseInt(year),
|
|
41
|
+
parseInt(month) - 1,
|
|
42
|
+
parseInt(day),
|
|
43
|
+
parseInt(hours),
|
|
44
|
+
parseInt(minutes),
|
|
45
|
+
parseInt(seconds)
|
|
46
|
+
));
|
|
47
|
+
|
|
48
|
+
// Verify the timestamp is reasonable (within last minute)
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const diff = now.getTime() - filenameDate.getTime();
|
|
51
|
+
expect(diff).toBeGreaterThanOrEqual(0);
|
|
52
|
+
expect(diff).toBeLessThan(60000); // Within last 60 seconds
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filename generation utilities for screenshots
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate UTC timestamp filename
|
|
7
|
+
* Format: screenshot-YYYY-MM-DD-HH-MM-SS-Z.jpg
|
|
8
|
+
*/
|
|
9
|
+
export function generateScreenshotFilename(date: Date = new Date()): string {
|
|
10
|
+
const year = date.getUTCFullYear();
|
|
11
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
12
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
13
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
14
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
15
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
16
|
+
|
|
17
|
+
return `screenshot-${year}-${month}-${day}-${hours}-${minutes}-${seconds}-Z.jpg`;
|
|
18
|
+
}
|
package/tests/adb.test.ts
CHANGED
|
@@ -28,8 +28,8 @@ describe('isPortListening', () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe('getCDPPort', () => {
|
|
31
|
-
it('should return the default CDP port', () => {
|
|
32
|
-
const port = getCDPPort();
|
|
31
|
+
it('should return the default CDP port', async () => {
|
|
32
|
+
const port = await getCDPPort();
|
|
33
33
|
expect(port).toBe(9223);
|
|
34
34
|
});
|
|
35
35
|
});
|
package/tests/exec.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ describe('execCommand', () => {
|
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
it('should reject on non-zero exit code', async () => {
|
|
11
|
-
await expect(execCommand('sh -c
|
|
11
|
+
await expect(execCommand('sh', ['-c', 'exit 1'])).rejects.toThrow();
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('should handle shell commands', async () => {
|
|
@@ -25,12 +25,12 @@ describe('execCommandFull', () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('should return non-zero exit code without throwing', async () => {
|
|
28
|
-
const result = await execCommandFull('sh -c
|
|
28
|
+
const result = await execCommandFull('sh', ['-c', 'exit 42']);
|
|
29
29
|
expect(result.code).toBe(42);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it('should capture stderr', async () => {
|
|
33
|
-
const result = await execCommandFull('sh -c
|
|
33
|
+
const result = await execCommandFull('sh', ['-c', 'echo error >&2']);
|
|
34
34
|
expect(result.stderr.trim()).toBe('error');
|
|
35
35
|
});
|
|
36
36
|
});
|