@myerscarpenter/quest-dev 1.0.8 → 1.2.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/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 +24 -0
- package/build/commands/logcat.d.ts.map +1 -0
- package/build/commands/logcat.js +261 -0
- package/build/commands/logcat.js.map +1 -0
- 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 +84 -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 -31
- package/build/utils/adb.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 +288 -0
- 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 +119 -9
- package/src/utils/adb.ts +148 -31
- package/src/utils/filename.test.ts +55 -0
- package/src/utils/filename.ts +18 -0
- package/tests/adb.test.ts +2 -2
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quest logcat command
|
|
3
|
+
* Captures Android logcat to files for Quest debugging
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: Quest's ring buffer fills in seconds under VR load.
|
|
6
|
+
* Always capture to a file BEFORE testing to avoid losing crash logs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolve, join } from 'path';
|
|
10
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, symlinkSync, statSync, readlinkSync, openSync } from 'fs';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { checkADBPath, checkADBDevices } from '../utils/adb.js';
|
|
13
|
+
import { execCommand, execCommandFull } from '../utils/exec.js';
|
|
14
|
+
|
|
15
|
+
const LOG_DIR = process.env.LOG_DIR || 'logs/logcat';
|
|
16
|
+
const PID_FILE = join(LOG_DIR, '.logcat_pid');
|
|
17
|
+
const LOGFILE_LINK = join(LOG_DIR, 'latest.txt');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ensure log directory exists
|
|
21
|
+
*/
|
|
22
|
+
function ensureLogDir(): void {
|
|
23
|
+
if (!existsSync(LOG_DIR)) {
|
|
24
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a process is running by PID
|
|
30
|
+
*/
|
|
31
|
+
function isProcessRunning(pid: number): boolean {
|
|
32
|
+
try {
|
|
33
|
+
// Sending signal 0 checks if process exists without killing it
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read PID from PID file
|
|
43
|
+
*/
|
|
44
|
+
function readPidFile(): number | null {
|
|
45
|
+
if (!existsSync(PID_FILE)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const pidStr = readFileSync(PID_FILE, 'utf-8').trim();
|
|
50
|
+
return parseInt(pidStr, 10);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write PID to PID file
|
|
58
|
+
*/
|
|
59
|
+
function writePidFile(pid: number): void {
|
|
60
|
+
writeFileSync(PID_FILE, pid.toString(), 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Delete PID file
|
|
65
|
+
*/
|
|
66
|
+
function deletePidFile(): void {
|
|
67
|
+
if (existsSync(PID_FILE)) {
|
|
68
|
+
unlinkSync(PID_FILE);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the latest log file path
|
|
74
|
+
*/
|
|
75
|
+
function getLatestLogFile(): string | null {
|
|
76
|
+
if (!existsSync(LOGFILE_LINK)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const target = readlinkSync(LOGFILE_LINK);
|
|
81
|
+
const fullPath = join(LOG_DIR, target);
|
|
82
|
+
if (existsSync(fullPath)) {
|
|
83
|
+
return fullPath;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Symlink might be broken
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get file size and line count
|
|
93
|
+
*/
|
|
94
|
+
function getFileStats(filePath: string): { size: string; lines: number } | null {
|
|
95
|
+
try {
|
|
96
|
+
const stats = statSync(filePath);
|
|
97
|
+
const sizeInBytes = stats.size;
|
|
98
|
+
let sizeStr: string;
|
|
99
|
+
|
|
100
|
+
if (sizeInBytes < 1024) {
|
|
101
|
+
sizeStr = `${sizeInBytes}B`;
|
|
102
|
+
} else if (sizeInBytes < 1024 * 1024) {
|
|
103
|
+
sizeStr = `${(sizeInBytes / 1024).toFixed(1)}K`;
|
|
104
|
+
} else {
|
|
105
|
+
sizeStr = `${(sizeInBytes / (1024 * 1024)).toFixed(1)}M`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
109
|
+
const lines = content.split('\n').length;
|
|
110
|
+
|
|
111
|
+
return { size: sizeStr, lines };
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Start logcat capture
|
|
119
|
+
*/
|
|
120
|
+
export async function startCommand(filter?: string): Promise<void> {
|
|
121
|
+
// Check for existing capture
|
|
122
|
+
const existingPid = readPidFile();
|
|
123
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
124
|
+
console.error('Already capturing. Use "quest-dev logcat stop" first.');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check prerequisites
|
|
129
|
+
checkADBPath();
|
|
130
|
+
await checkADBDevices();
|
|
131
|
+
|
|
132
|
+
ensureLogDir();
|
|
133
|
+
|
|
134
|
+
// Generate log filename
|
|
135
|
+
const timestamp = new Date().toISOString()
|
|
136
|
+
.replace(/[-:]/g, '')
|
|
137
|
+
.replace(/\..+/, '')
|
|
138
|
+
.replace('T', '_')
|
|
139
|
+
.slice(0, 15); // YYYYMMDD_HHMMSS
|
|
140
|
+
const logFile = join(LOG_DIR, `logcat_${timestamp}.txt`);
|
|
141
|
+
|
|
142
|
+
console.log(`Starting capture to: ${logFile}`);
|
|
143
|
+
|
|
144
|
+
// Clear the buffer first - critical for Quest
|
|
145
|
+
try {
|
|
146
|
+
await execCommand('adb', ['logcat', '-c']);
|
|
147
|
+
console.log('Ring buffer cleared.');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Failed to clear ring buffer:', (error as Error).message);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (filter) {
|
|
154
|
+
console.log(`Filter: ${filter}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Start background logcat process
|
|
158
|
+
const args = ['logcat', '-v', 'threadtime'];
|
|
159
|
+
if (filter) {
|
|
160
|
+
args.push(filter);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Open file for writing
|
|
164
|
+
const fd = openSync(logFile, 'w');
|
|
165
|
+
|
|
166
|
+
const proc = spawn('adb', args, {
|
|
167
|
+
stdio: ['ignore', fd, fd],
|
|
168
|
+
detached: true
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Unref so parent can exit immediately
|
|
172
|
+
proc.unref();
|
|
173
|
+
|
|
174
|
+
// Save PID
|
|
175
|
+
writePidFile(proc.pid!);
|
|
176
|
+
|
|
177
|
+
// Update symlink
|
|
178
|
+
try {
|
|
179
|
+
if (existsSync(LOGFILE_LINK)) {
|
|
180
|
+
unlinkSync(LOGFILE_LINK);
|
|
181
|
+
}
|
|
182
|
+
symlinkSync(`logcat_${timestamp}.txt`, LOGFILE_LINK);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.warn('Warning: Failed to create symlink:', (error as Error).message);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log(`Capturing (PID: ${proc.pid})`);
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('Now run your test. When done: quest-dev logcat stop');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Stop logcat capture
|
|
194
|
+
*/
|
|
195
|
+
export async function stopCommand(): Promise<void> {
|
|
196
|
+
const pid = readPidFile();
|
|
197
|
+
|
|
198
|
+
if (!pid) {
|
|
199
|
+
console.log('No capture in progress');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (isProcessRunning(pid)) {
|
|
204
|
+
try {
|
|
205
|
+
process.kill(pid, 'SIGTERM');
|
|
206
|
+
console.log(`Capture stopped (PID: ${pid})`);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.log(`Capture process already ended (PID: ${pid})`);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
console.log('Capture process already ended');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
deletePidFile();
|
|
215
|
+
|
|
216
|
+
// Show file info
|
|
217
|
+
const latestFile = getLatestLogFile();
|
|
218
|
+
if (latestFile) {
|
|
219
|
+
const stats = getFileStats(latestFile);
|
|
220
|
+
if (stats) {
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(`Log file: ${latestFile}`);
|
|
223
|
+
console.log(`Size: ${stats.size} (${stats.lines} lines)`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Show capture status
|
|
230
|
+
*/
|
|
231
|
+
export async function statusCommand(): Promise<void> {
|
|
232
|
+
const pid = readPidFile();
|
|
233
|
+
|
|
234
|
+
if (pid && isProcessRunning(pid)) {
|
|
235
|
+
console.log(`Capturing (PID: ${pid})`);
|
|
236
|
+
|
|
237
|
+
const latestFile = getLatestLogFile();
|
|
238
|
+
if (latestFile) {
|
|
239
|
+
const stats = getFileStats(latestFile);
|
|
240
|
+
if (stats) {
|
|
241
|
+
console.log(`File: ${latestFile}`);
|
|
242
|
+
console.log(`Size: ${stats.size} (${stats.lines} lines)`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
console.log('Not capturing');
|
|
247
|
+
|
|
248
|
+
// Show recent logs
|
|
249
|
+
if (existsSync(LOG_DIR)) {
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log('Recent logs:');
|
|
252
|
+
try {
|
|
253
|
+
const result = await execCommandFull('ls', ['-lht', join(LOG_DIR, '*.txt')]);
|
|
254
|
+
if (result.code === 0) {
|
|
255
|
+
const lines = result.stdout.trim().split('\n').slice(0, 5);
|
|
256
|
+
lines.forEach(line => console.log(' ' + line));
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// Ignore
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Tail current capture
|
|
267
|
+
*/
|
|
268
|
+
export async function tailCommand(): Promise<void> {
|
|
269
|
+
const latestFile = getLatestLogFile();
|
|
270
|
+
|
|
271
|
+
if (!latestFile) {
|
|
272
|
+
console.error('No active log file');
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(`Tailing: ${latestFile}`);
|
|
277
|
+
console.log('Press Ctrl+C to stop\n');
|
|
278
|
+
|
|
279
|
+
// Use tail -f
|
|
280
|
+
const tailProc = spawn('tail', ['-f', latestFile], {
|
|
281
|
+
stdio: 'inherit'
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
tailProc.on('error', (error) => {
|
|
285
|
+
console.error('Failed to tail log:', error.message);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
});
|
|
288
|
+
}
|
package/src/commands/open.ts
CHANGED
|
@@ -17,8 +17,8 @@ import { execCommand, execCommandFull } from '../utils/exec.js';
|
|
|
17
17
|
/**
|
|
18
18
|
* Close all tabs except the one with the target URL
|
|
19
19
|
*/
|
|
20
|
-
async function closeOtherTabs(targetUrl: string): Promise<void> {
|
|
21
|
-
const cdpPort = getCDPPort();
|
|
20
|
+
async function closeOtherTabs(targetUrl: string, browser: string): Promise<void> {
|
|
21
|
+
const cdpPort = await getCDPPort(browser);
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
24
|
// Get list of tabs
|
|
@@ -66,8 +66,8 @@ async function closeOtherTabs(targetUrl: string): Promise<void> {
|
|
|
66
66
|
/**
|
|
67
67
|
* Try to navigate or reload existing tab via cdp-cli
|
|
68
68
|
*/
|
|
69
|
-
async function tryNavigateExistingTab(targetUrl: string): Promise<boolean> {
|
|
70
|
-
const cdpPort = getCDPPort();
|
|
69
|
+
async function tryNavigateExistingTab(targetUrl: string, browser: string): Promise<boolean> {
|
|
70
|
+
const cdpPort = await getCDPPort(browser);
|
|
71
71
|
|
|
72
72
|
try {
|
|
73
73
|
// Get list of tabs using cdp-cli
|
|
@@ -142,7 +142,11 @@ async function tryNavigateExistingTab(targetUrl: string): Promise<boolean> {
|
|
|
142
142
|
/**
|
|
143
143
|
* Main open command handler
|
|
144
144
|
*/
|
|
145
|
-
export async function openCommand(
|
|
145
|
+
export async function openCommand(
|
|
146
|
+
url: string,
|
|
147
|
+
closeOthers: boolean = false,
|
|
148
|
+
browser: string = 'com.oculus.browser'
|
|
149
|
+
): Promise<void> {
|
|
146
150
|
// Parse URL to determine if we need reverse port forwarding
|
|
147
151
|
let parsedUrl: URL;
|
|
148
152
|
try {
|
|
@@ -175,27 +179,27 @@ export async function openCommand(url: string, closeOthers: boolean = false): Pr
|
|
|
175
179
|
// Set up port forwarding
|
|
176
180
|
if (port !== null) {
|
|
177
181
|
// Localhost URL: need reverse forwarding so Quest can reach the dev server
|
|
178
|
-
await ensurePortForwarding(port);
|
|
182
|
+
await ensurePortForwarding(port, browser);
|
|
179
183
|
} else {
|
|
180
184
|
// External URL: only need CDP forwarding to control the browser
|
|
181
|
-
await ensureCDPForwarding();
|
|
185
|
+
await ensureCDPForwarding(browser);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
// Check if browser is running
|
|
185
|
-
const browserRunning = await isBrowserRunning();
|
|
189
|
+
const browserRunning = await isBrowserRunning(browser);
|
|
186
190
|
|
|
187
191
|
if (!browserRunning) {
|
|
188
|
-
console.log('
|
|
189
|
-
await launchBrowser(url);
|
|
192
|
+
console.log('Browser is not running');
|
|
193
|
+
await launchBrowser(url, browser);
|
|
190
194
|
} else {
|
|
191
|
-
console.log('
|
|
195
|
+
console.log('Browser is already running');
|
|
192
196
|
|
|
193
197
|
// Try to navigate existing or blank tab via cdp-cli first
|
|
194
|
-
const navigated = await tryNavigateExistingTab(url);
|
|
198
|
+
const navigated = await tryNavigateExistingTab(url, browser);
|
|
195
199
|
|
|
196
200
|
if (!navigated) {
|
|
197
201
|
console.log('No existing or blank tab found, opening URL...');
|
|
198
|
-
await launchBrowser(url);
|
|
202
|
+
await launchBrowser(url, browser);
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
@@ -204,7 +208,7 @@ export async function openCommand(url: string, closeOthers: boolean = false): Pr
|
|
|
204
208
|
// Wait for browser to stabilize after launch/navigation
|
|
205
209
|
console.log('Waiting for browser to stabilize...');
|
|
206
210
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
207
|
-
await closeOtherTabs(url);
|
|
211
|
+
await closeOtherTabs(url, browser);
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
console.log('\nDone!\n');
|
|
@@ -3,9 +3,33 @@
|
|
|
3
3
|
* Triggers Quest's native screenshot service and pulls the file
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { resolve } from 'path';
|
|
6
|
+
import { resolve, join } from 'path';
|
|
7
|
+
import { existsSync, statSync } from 'fs';
|
|
7
8
|
import { checkADBPath, checkADBDevices, checkUSBFileTransfer, checkQuestAwake } from '../utils/adb.js';
|
|
8
9
|
import { execCommand, execCommandFull } from '../utils/exec.js';
|
|
10
|
+
import { generateScreenshotFilename } from '../utils/filename.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate directory exists and is writable
|
|
14
|
+
*/
|
|
15
|
+
function validateDirectory(dirPath: string): void {
|
|
16
|
+
const resolvedPath = resolve(dirPath);
|
|
17
|
+
|
|
18
|
+
if (!existsSync(resolvedPath)) {
|
|
19
|
+
console.error(`Error: Directory does not exist: ${resolvedPath}`);
|
|
20
|
+
console.error('');
|
|
21
|
+
console.error('Please create the directory first.');
|
|
22
|
+
console.error('');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const stat = statSync(resolvedPath);
|
|
27
|
+
if (!stat.isDirectory()) {
|
|
28
|
+
console.error(`Error: Path is not a directory: ${resolvedPath}`);
|
|
29
|
+
console.error('');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
9
33
|
|
|
10
34
|
/**
|
|
11
35
|
* Trigger Quest screenshot service
|
|
@@ -106,14 +130,37 @@ async function deleteRemoteScreenshot(filename: string): Promise<void> {
|
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Add caption to JPEG COM metadata
|
|
135
|
+
*/
|
|
136
|
+
async function addJpegMetadata(filePath: string, caption: string): Promise<boolean> {
|
|
137
|
+
try {
|
|
138
|
+
const { exiftool } = await import('exiftool-vendored');
|
|
139
|
+
await exiftool.write(filePath, { Comment: caption });
|
|
140
|
+
await exiftool.end();
|
|
141
|
+
console.log(`Caption added: "${caption}"`);
|
|
142
|
+
return true;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Warning: Failed to add caption metadata:', (error as Error).message);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
/**
|
|
110
150
|
* Main screenshot command handler
|
|
111
151
|
*/
|
|
112
|
-
export async function screenshotCommand(
|
|
113
|
-
const
|
|
152
|
+
export async function screenshotCommand(directoryPath: string, caption: string | undefined): Promise<void> {
|
|
153
|
+
const resolvedDir = resolve(directoryPath);
|
|
114
154
|
|
|
115
155
|
console.log('\nQuest Screenshot\n');
|
|
116
156
|
|
|
157
|
+
// Validate directory (fail-fast before expensive ADB ops)
|
|
158
|
+
validateDirectory(resolvedDir);
|
|
159
|
+
|
|
160
|
+
// Generate filename
|
|
161
|
+
const localFilename = generateScreenshotFilename();
|
|
162
|
+
const outputPath = join(resolvedDir, localFilename);
|
|
163
|
+
|
|
117
164
|
// Check prerequisites
|
|
118
165
|
checkADBPath();
|
|
119
166
|
await checkADBDevices();
|
|
@@ -130,7 +177,7 @@ export async function screenshotCommand(outputPath: string): Promise<void> {
|
|
|
130
177
|
|
|
131
178
|
// Wait for screenshot to save and verify it's complete (has JPEG EOI marker)
|
|
132
179
|
console.log('Waiting for screenshot to save...');
|
|
133
|
-
let
|
|
180
|
+
let questFilename: string | null = null;
|
|
134
181
|
const maxAttempts = 20;
|
|
135
182
|
|
|
136
183
|
for (let i = 0; i < maxAttempts; i++) {
|
|
@@ -141,14 +188,14 @@ export async function screenshotCommand(outputPath: string): Promise<void> {
|
|
|
141
188
|
// Check that the JPEG is fully written (has EOI marker)
|
|
142
189
|
const complete = await isJpegComplete(newScreenshot);
|
|
143
190
|
if (complete) {
|
|
144
|
-
|
|
145
|
-
console.log(`Screenshot ready: ${
|
|
191
|
+
questFilename = newScreenshot;
|
|
192
|
+
console.log(`Screenshot ready: ${questFilename}`);
|
|
146
193
|
break;
|
|
147
194
|
}
|
|
148
195
|
}
|
|
149
196
|
}
|
|
150
197
|
|
|
151
|
-
if (!
|
|
198
|
+
if (!questFilename) {
|
|
152
199
|
console.error('Error: Screenshot was not created or is incomplete');
|
|
153
200
|
console.error('');
|
|
154
201
|
console.error('The screenshot service was triggered but no valid screenshot appeared.');
|
|
@@ -162,12 +209,17 @@ export async function screenshotCommand(outputPath: string): Promise<void> {
|
|
|
162
209
|
}
|
|
163
210
|
|
|
164
211
|
// Pull screenshot
|
|
165
|
-
if (!await pullScreenshot(
|
|
212
|
+
if (!await pullScreenshot(questFilename, outputPath)) {
|
|
166
213
|
process.exit(1);
|
|
167
214
|
}
|
|
168
215
|
|
|
216
|
+
// Add metadata (non-fatal, only if caption provided)
|
|
217
|
+
if (caption) {
|
|
218
|
+
await addJpegMetadata(outputPath, caption);
|
|
219
|
+
}
|
|
220
|
+
|
|
169
221
|
// Delete from Quest after successful pull
|
|
170
|
-
await deleteRemoteScreenshot(
|
|
222
|
+
await deleteRemoteScreenshot(questFilename);
|
|
171
223
|
|
|
172
224
|
console.log('\nDone!\n');
|
|
173
225
|
}
|