@myerscarpenter/quest-dev 1.4.1 → 2.0.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.
Files changed (142) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.github/workflows/docs.yml +45 -0
  3. package/.github/workflows/publish.yml +11 -1
  4. package/README.md +27 -0
  5. package/build/cast/decoder.d.ts +48 -0
  6. package/build/cast/decoder.d.ts.map +1 -0
  7. package/build/cast/decoder.js +152 -0
  8. package/build/cast/decoder.js.map +1 -0
  9. package/build/cast/session.d.ts +87 -0
  10. package/build/cast/session.d.ts.map +1 -0
  11. package/build/cast/session.js +565 -0
  12. package/build/cast/session.js.map +1 -0
  13. package/build/commands/logcat.d.ts.map +1 -1
  14. package/build/commands/logcat.js +7 -6
  15. package/build/commands/logcat.js.map +1 -1
  16. package/build/commands/screenshot.d.ts.map +1 -1
  17. package/build/commands/screenshot.js +17 -20
  18. package/build/commands/screenshot.js.map +1 -1
  19. package/build/commands/stay-awake.d.ts +2 -15
  20. package/build/commands/stay-awake.d.ts.map +1 -1
  21. package/build/commands/stay-awake.js +14 -77
  22. package/build/commands/stay-awake.js.map +1 -1
  23. package/build/daemon/cast-manager.d.ts +42 -0
  24. package/build/daemon/cast-manager.d.ts.map +1 -0
  25. package/build/daemon/cast-manager.js +243 -0
  26. package/build/daemon/cast-manager.js.map +1 -0
  27. package/build/daemon/client.d.ts +40 -0
  28. package/build/daemon/client.d.ts.map +1 -0
  29. package/build/daemon/client.js +133 -0
  30. package/build/daemon/client.js.map +1 -0
  31. package/build/daemon/daemon.d.ts +20 -0
  32. package/build/daemon/daemon.d.ts.map +1 -0
  33. package/build/daemon/daemon.js +130 -0
  34. package/build/daemon/daemon.js.map +1 -0
  35. package/build/daemon/deploy.d.ts +44 -0
  36. package/build/daemon/deploy.d.ts.map +1 -0
  37. package/build/daemon/deploy.js +230 -0
  38. package/build/daemon/deploy.js.map +1 -0
  39. package/build/daemon/logcat-manager.d.ts +39 -0
  40. package/build/daemon/logcat-manager.d.ts.map +1 -0
  41. package/build/daemon/logcat-manager.js +194 -0
  42. package/build/daemon/logcat-manager.js.map +1 -0
  43. package/build/daemon/server.d.ts +19 -0
  44. package/build/daemon/server.d.ts.map +1 -0
  45. package/build/daemon/server.js +482 -0
  46. package/build/daemon/server.js.map +1 -0
  47. package/build/daemon/stay-awake-manager.d.ts +22 -0
  48. package/build/daemon/stay-awake-manager.d.ts.map +1 -0
  49. package/build/daemon/stay-awake-manager.js +74 -0
  50. package/build/daemon/stay-awake-manager.js.map +1 -0
  51. package/build/index.js +272 -45
  52. package/build/index.js.map +1 -1
  53. package/build/public/dashboard.js +749 -0
  54. package/build/public/index.html +12 -0
  55. package/build/public/style.css +106 -0
  56. package/build/utils/adb.d.ts +6 -0
  57. package/build/utils/adb.d.ts.map +1 -1
  58. package/build/utils/adb.js +62 -66
  59. package/build/utils/adb.js.map +1 -1
  60. package/build/utils/casting-apk.d.ts +40 -0
  61. package/build/utils/casting-apk.d.ts.map +1 -0
  62. package/build/utils/casting-apk.js +252 -0
  63. package/build/utils/casting-apk.js.map +1 -0
  64. package/build/utils/config.d.ts +5 -3
  65. package/build/utils/config.d.ts.map +1 -1
  66. package/build/utils/config.js +18 -38
  67. package/build/utils/config.js.map +1 -1
  68. package/build/utils/exec.d.ts +5 -0
  69. package/build/utils/exec.d.ts.map +1 -1
  70. package/build/utils/exec.js +17 -0
  71. package/build/utils/exec.js.map +1 -1
  72. package/build/utils/filename.d.ts +7 -1
  73. package/build/utils/filename.d.ts.map +1 -1
  74. package/build/utils/filename.js +17 -2
  75. package/build/utils/filename.js.map +1 -1
  76. package/build/utils/filename.test.js +33 -1
  77. package/build/utils/filename.test.js.map +1 -1
  78. package/build/utils/jpeg-comment.d.ts +14 -0
  79. package/build/utils/jpeg-comment.d.ts.map +1 -0
  80. package/build/utils/jpeg-comment.js +28 -0
  81. package/build/utils/jpeg-comment.js.map +1 -0
  82. package/build/utils/test-properties.d.ts +34 -0
  83. package/build/utils/test-properties.d.ts.map +1 -0
  84. package/build/utils/test-properties.js +73 -0
  85. package/build/utils/test-properties.js.map +1 -0
  86. package/package.json +11 -5
  87. package/packages/cast2-protocol/README.md +86 -0
  88. package/packages/cast2-protocol/docs/_config.yml +4 -0
  89. package/packages/cast2-protocol/docs/feature-flags.md +102 -0
  90. package/packages/cast2-protocol/docs/index.md +24 -0
  91. package/packages/cast2-protocol/docs/open-investigations.md +149 -0
  92. package/packages/cast2-protocol/docs/protocol.md +602 -0
  93. package/packages/cast2-protocol/package.json +46 -0
  94. package/packages/cast2-protocol/src/constants.ts +65 -0
  95. package/packages/cast2-protocol/src/index.ts +7 -0
  96. package/packages/cast2-protocol/src/mgik.ts +69 -0
  97. package/packages/cast2-protocol/src/mud.ts +294 -0
  98. package/packages/cast2-protocol/src/pose.ts +99 -0
  99. package/packages/cast2-protocol/src/resolutions.ts +34 -0
  100. package/packages/cast2-protocol/src/types.ts +64 -0
  101. package/packages/cast2-protocol/src/xrsp.ts +73 -0
  102. package/packages/cast2-protocol/tests/mgik.test.ts +80 -0
  103. package/packages/cast2-protocol/tests/mud.test.ts +295 -0
  104. package/packages/cast2-protocol/tests/pose.test.ts +173 -0
  105. package/packages/cast2-protocol/tests/xrsp.test.ts +90 -0
  106. package/packages/cast2-protocol/tsconfig.json +20 -0
  107. package/pnpm-workspace.yaml +2 -0
  108. package/src/cast/decoder.ts +178 -0
  109. package/src/cast/session.ts +708 -0
  110. package/src/commands/logcat.ts +6 -5
  111. package/src/commands/screenshot.ts +19 -13
  112. package/src/commands/stay-awake.ts +22 -91
  113. package/src/daemon/adbkit-apkreader.d.ts +14 -0
  114. package/src/daemon/cast-manager.ts +282 -0
  115. package/src/daemon/client.ts +166 -0
  116. package/src/daemon/daemon.ts +169 -0
  117. package/src/daemon/deploy.ts +307 -0
  118. package/src/daemon/logcat-manager.ts +229 -0
  119. package/src/daemon/server.ts +595 -0
  120. package/src/daemon/stay-awake-manager.ts +83 -0
  121. package/src/index.ts +326 -56
  122. package/src/public/dashboard.js +288 -0
  123. package/src/public/index.html +12 -0
  124. package/src/public/style.css +106 -0
  125. package/src/utils/adb.ts +70 -57
  126. package/src/utils/casting-apk.ts +276 -0
  127. package/src/utils/config.ts +18 -36
  128. package/src/utils/exec.ts +20 -0
  129. package/src/utils/filename.test.ts +41 -1
  130. package/src/utils/filename.ts +18 -2
  131. package/src/utils/jpeg-comment.ts +30 -0
  132. package/src/utils/test-properties.ts +94 -0
  133. package/tests/cast/auto-layer.test.ts +87 -0
  134. package/tests/cast/decoder.test.ts +82 -0
  135. package/tests/cast/session-restart.test.ts +107 -0
  136. package/tests/config.test.ts +17 -22
  137. package/tests/daemon/api-status.test.ts +82 -0
  138. package/tests/daemon/cast-manager.test.ts +69 -0
  139. package/tests/daemon/mjpeg-stream.test.ts +144 -0
  140. package/tests/daemon/pose-endpoint.test.ts +63 -0
  141. package/tests/daemon/start-guard.test.ts +77 -0
  142. package/vitest.config.ts +10 -0
package/src/index.ts CHANGED
@@ -9,14 +9,17 @@ import yargs from 'yargs';
9
9
  import { hideBin } from 'yargs/helpers';
10
10
  import { readFileSync } from 'fs';
11
11
  import { fileURLToPath } from 'url';
12
- import { dirname, join } from 'path';
12
+ import { dirname, join, resolve } from 'path';
13
13
  import { screenshotCommand } from './commands/screenshot.js';
14
14
  import { openCommand } from './commands/open.js';
15
- import { startCommand, stopCommand, statusCommand, tailCommand } from './commands/logcat.js';
15
+ import { tailCommand } from './commands/logcat.js';
16
16
  import { batteryCommand } from './commands/battery.js';
17
- import { stayAwakeCommand, stayAwakeWatchdog, stayAwakeStatus, stayAwakeDisable } from './commands/stay-awake.js';
18
- import { saveConfig, loadConfig } from './utils/config.js';
17
+ import { stayAwakeStatus, stayAwakeDisable } from './commands/stay-awake.js';
18
+ import { saveConfig, loadConfig, type QuestDevConfig } from './utils/config.js';
19
19
  import { setVerbose } from './utils/verbose.js';
20
+ import { ensureDaemon, daemonRequest, discoverDaemon, daemonFetch, resolvePort, resolveHost } from './daemon/client.js';
21
+ import { startDaemon } from './daemon/daemon.js';
22
+ import { extractCastingApk, hasCastingApk, findInstalledMqdh } from './utils/casting-apk.js';
20
23
 
21
24
  // Read version from package.json
22
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -38,6 +41,21 @@ const cli = yargs(hideBin(process.argv))
38
41
  default: false,
39
42
  global: true,
40
43
  })
44
+ .option('port', {
45
+ describe: 'Daemon HTTP port (or save with: quest-dev config --port)',
46
+ type: 'number',
47
+ global: true,
48
+ })
49
+ .option('device', {
50
+ describe: 'Quest IP address (or save with: quest-dev config --device)',
51
+ type: 'string',
52
+ global: true,
53
+ })
54
+ .option('host', {
55
+ describe: 'Daemon bind address (default: 127.0.0.1, use 0.0.0.0 for network access)',
56
+ type: 'string',
57
+ global: true,
58
+ })
41
59
  .fail((msg, err, yargs) => {
42
60
  yargs.showHelp();
43
61
  if (err) console.error(err.message);
@@ -45,9 +63,9 @@ const cli = yargs(hideBin(process.argv))
45
63
  })
46
64
  .help()
47
65
  .alias('help', 'h')
48
- .epilog('Requires ADB and Quest connected via USB. Sets up CDP on port 9223 for cdp-cli.');
66
+ .epilog('Requires ADB and Quest connected via USB or Wi-Fi (adb connect).');
49
67
 
50
- // Screenshot command
68
+ // Screenshot command (standalone — no daemon needed)
51
69
  cli.command(
52
70
  'screenshot <directory>',
53
71
  'Take a screenshot from Quest and save to directory with auto-generated filename',
@@ -72,7 +90,7 @@ cli.command(
72
90
  }
73
91
  );
74
92
 
75
- // Open command
93
+ // Open command (standalone)
76
94
  cli.command(
77
95
  'open <url>',
78
96
  'Open URL in Quest browser (sets up CDP debugging port forwarding)',
@@ -104,7 +122,7 @@ cli.command(
104
122
  }
105
123
  );
106
124
 
107
- // Logcat command
125
+ // Logcat command — delegates to daemon for start/stop/status, tail remains standalone
108
126
  cli.command(
109
127
  'logcat <action>',
110
128
  'Capture Android logcat to files (CRITICAL: always start before testing to avoid losing crash logs)',
@@ -123,29 +141,73 @@ cli.command(
123
141
  },
124
142
  async (argv) => {
125
143
  const action = argv.action as string;
126
- const filter = argv.filter as string | undefined;
127
144
 
145
+ if (action === 'tail') {
146
+ await tailCommand();
147
+ return;
148
+ }
149
+
150
+ // Delegate to daemon
151
+ const info = await ensureDaemon({ port: argv.port as number | undefined, device: argv.device as string | undefined, host: argv.host as string | undefined });
128
152
  switch (action) {
129
- case 'start':
130
- await startCommand(filter);
131
- break;
132
- case 'stop':
133
- await stopCommand();
153
+ case 'start': {
154
+ const result = await daemonFetch(info, '/logcat/start', {
155
+ body: { filter: argv.filter },
156
+ }) as { ok: boolean; file?: string; pid?: number };
157
+ if (result.ok) {
158
+ console.log(`Capturing (PID: ${result.pid})`);
159
+ console.log(`File: ${result.file}`);
160
+ console.log('');
161
+ console.log('Now run your test. When done: quest-dev logcat stop');
162
+ } else {
163
+ console.error('Failed to start logcat capture');
164
+ console.error(JSON.stringify(result, null, 2));
165
+ }
134
166
  break;
135
- case 'status':
136
- await statusCommand();
167
+ }
168
+ case 'stop': {
169
+ const result = await daemonFetch(info, '/logcat/stop', {
170
+ method: 'POST',
171
+ }) as { ok: boolean };
172
+ if (result.ok) {
173
+ // Show file info
174
+ const status = await daemonFetch(info, '/logcat/status') as {
175
+ file?: string;
176
+ size?: string;
177
+ lines?: number;
178
+ };
179
+ console.log('Capture stopped');
180
+ if (status.file) {
181
+ console.log(`File: ${status.file}`);
182
+ if (status.size) console.log(`Size: ${status.size}`);
183
+ }
184
+ }
137
185
  break;
138
- case 'tail':
139
- await tailCommand();
186
+ }
187
+ case 'status': {
188
+ const result = await daemonFetch(info, '/logcat/status') as {
189
+ capturing: boolean;
190
+ pid?: number;
191
+ file?: string;
192
+ size?: string;
193
+ lines?: number;
194
+ };
195
+ if (result.capturing) {
196
+ console.log(`Capturing (PID: ${result.pid})`);
197
+ if (result.file) {
198
+ console.log(`File: ${result.file}`);
199
+ if (result.size) console.log(`Size: ${result.size}`);
200
+ }
201
+ } else {
202
+ console.log('Not capturing');
203
+ }
140
204
  break;
141
- default:
142
- console.error(`Unknown action: ${action}`);
143
- process.exit(1);
205
+ }
144
206
  }
145
207
  }
146
208
  );
147
209
 
148
- // Battery command
210
+ // Battery command (standalone)
149
211
  cli.command(
150
212
  'battery',
151
213
  'Show Quest battery percentage and charging status',
@@ -155,10 +217,42 @@ cli.command(
155
217
  }
156
218
  );
157
219
 
158
- // Stay-awake command
220
+ // Start command — starts daemon with stay-awake for web content workflows
221
+ cli.command(
222
+ 'start',
223
+ 'Start quest-dev daemon (enables stay-awake, serves dashboard for casting)',
224
+ (yargs) => {
225
+ return yargs
226
+ .option('pin', {
227
+ describe: 'Meta Store PIN for stay-awake (or save with: quest-dev config --pin)',
228
+ type: 'string',
229
+ });
230
+ },
231
+ async (argv) => {
232
+ const info = await ensureDaemon({ port: argv.port as number | undefined, device: argv.device as string | undefined, host: argv.host as string | undefined });
233
+
234
+ // Enable stay-awake
235
+ const result = await daemonFetch(info, '/stay-awake/enable', {
236
+ body: { pin: argv.pin },
237
+ }) as { ok: boolean; error?: string };
238
+ if (result.ok) {
239
+ console.log('Stay-awake enabled');
240
+ } else if (result.error !== 'PIN required') {
241
+ console.warn('Stay-awake:', result.error);
242
+ }
243
+
244
+ const url = `http://localhost:${info.port}/`;
245
+ console.log(`\nDaemon running (PID: ${info.pid}, port: ${info.port})`);
246
+ console.log(`Dashboard: ${url}`);
247
+ console.log(`\nStart casting: curl -X POST ${url}cast/start`);
248
+ console.log(`Stop daemon: quest-dev stop`);
249
+ }
250
+ );
251
+
252
+ // Stay-awake command — delegates to daemon
159
253
  cli.command(
160
254
  'stay-awake',
161
- 'Keep Quest awake (disables autosleep, guardian, dialogs)',
255
+ 'Keep Quest awake (disables autosleep, guardian, dialogs) via daemon',
162
256
  (yargs) => {
163
257
  return yargs
164
258
  .option('pin', {
@@ -184,30 +278,149 @@ cli.command(
184
278
  type: 'boolean',
185
279
  default: false,
186
280
  })
187
- .option('verbose', {
188
- describe: 'Print battery level on every check (every 60s)',
189
- type: 'boolean',
190
- default: false,
191
- alias: 'v',
192
- });
281
+ ;
193
282
  },
194
283
  async (argv) => {
195
284
  if (argv.status) {
196
285
  await stayAwakeStatus();
197
- } else if (argv.disable) {
198
- await stayAwakeDisable(argv.pin as string | undefined);
286
+ return;
287
+ }
288
+ if (argv.disable) {
289
+ // Try daemon first, fall back to direct
290
+ const existing = discoverDaemon();
291
+ if (existing) {
292
+ await daemonFetch(existing, '/stay-awake/disable', { method: 'POST' });
293
+ console.log('Stay-awake disabled via daemon');
294
+ } else {
295
+ await stayAwakeDisable(argv.pin as string | undefined);
296
+ }
297
+ return;
298
+ }
299
+
300
+ // Enable via daemon
301
+ const info = await ensureDaemon({
302
+ port: argv.port as number | undefined,
303
+ device: argv.device as string | undefined,
304
+ host: argv.host as string | undefined,
305
+ idleTimeout: argv.idleTimeout as number | undefined,
306
+ lowBattery: argv.lowBattery as number | undefined,
307
+ });
308
+ const result = await daemonFetch(info, '/stay-awake/enable', {
309
+ body: { pin: argv.pin },
310
+ }) as { ok: boolean; error?: string };
311
+ if (result.ok) {
312
+ console.log('Stay-awake enabled via daemon');
313
+ console.log(`Daemon PID: ${info.pid}, port: ${info.port}`);
199
314
  } else {
200
- await stayAwakeCommand(
201
- argv.pin as string | undefined,
202
- argv.idleTimeout as number | undefined,
203
- argv.lowBattery as number | undefined,
204
- argv.verbose as boolean,
205
- );
315
+ console.error('Failed to enable stay-awake:', result.error);
316
+ process.exit(1);
206
317
  }
207
318
  }
208
319
  );
209
320
 
210
- // Config command
321
+ // Deploy command — auto-starts daemon, deploys APK, reports crash/success
322
+ cli.command(
323
+ 'deploy <apk>',
324
+ 'Deploy APK to Quest (auto-starts daemon, enables stay-awake, installs, launches, checks for crash)',
325
+ (yargs) => {
326
+ return yargs
327
+ .positional('apk', {
328
+ describe: 'Path to APK file',
329
+ type: 'string',
330
+ demandOption: true,
331
+ })
332
+ .option('crash-wait', {
333
+ describe: 'Time in ms to wait before crash check (default: 5000)',
334
+ type: 'number',
335
+ default: 5000,
336
+ });
337
+ },
338
+ async (argv) => {
339
+ const apkPath = resolve(argv.apk as string);
340
+ const info = await ensureDaemon({ port: argv.port as number | undefined, device: argv.device as string | undefined, host: argv.host as string | undefined });
341
+
342
+ console.log(`Deploying: ${apkPath}`);
343
+ const result = await daemonFetch(info, '/deploy', {
344
+ body: {
345
+ apk_path: apkPath,
346
+ crash_wait_ms: argv.crashWait,
347
+ },
348
+ }) as {
349
+ ok: boolean;
350
+ package: string;
351
+ crashed: boolean;
352
+ logcatLines?: string[];
353
+ logcatFile?: string;
354
+ error?: string;
355
+ install?: {
356
+ incremental: boolean;
357
+ blocksTransferred?: number;
358
+ totalBlocks?: number;
359
+ bytesTransferred?: number;
360
+ installSecs: number;
361
+ apkSizeMB: number;
362
+ };
363
+ };
364
+
365
+ if (result.ok) {
366
+ const inst = result.install;
367
+ if (inst) {
368
+ const mode = inst.incremental ? 'incremental' : 'full';
369
+ if (inst.blocksTransferred !== undefined && inst.totalBlocks) {
370
+ const kb = Math.round((inst.bytesTransferred ?? 0) / 1024);
371
+ console.log(`\nInstalled (${mode}, ${inst.installSecs}s): ${inst.blocksTransferred}/${inst.totalBlocks} blocks (~${kb}KB of ${inst.apkSizeMB}MB)`);
372
+ } else {
373
+ console.log(`\nInstalled (${mode}, ${inst.installSecs}s)`);
374
+ }
375
+ }
376
+ console.log(`Deploy successful: ${result.package} is running`);
377
+ console.log(`Logcat: ${result.logcatFile}`);
378
+ console.log(`Daemon API: http://127.0.0.1:${info.port}/help`);
379
+ } else if (result.crashed) {
380
+ console.error(`\nCRASH DETECTED: ${result.package}`);
381
+ if (result.logcatLines && result.logcatLines.length > 0) {
382
+ console.error(`\n--- ${result.logcatFile} ---`);
383
+ for (const line of result.logcatLines) {
384
+ console.error(line);
385
+ }
386
+ console.error(`--- End ${result.logcatFile} ---\n`);
387
+ }
388
+ if (result.error) {
389
+ console.error(result.error);
390
+ }
391
+ console.error(`Logcat: ${result.logcatFile}`);
392
+ process.exit(1);
393
+ } else {
394
+ console.error(`\nDeploy failed: ${result.error}`);
395
+ if (result.logcatFile) {
396
+ console.error(`Logcat: ${result.logcatFile}`);
397
+ }
398
+ process.exit(1);
399
+ }
400
+ }
401
+ );
402
+
403
+ // Stop command — shuts down daemon
404
+ cli.command(
405
+ 'stop',
406
+ 'Stop the quest-dev daemon (restores Quest settings)',
407
+ () => {},
408
+ async () => {
409
+ const existing = discoverDaemon();
410
+ if (!existing) {
411
+ console.log('No daemon running');
412
+ return;
413
+ }
414
+ try {
415
+ await daemonFetch(existing, '/shutdown', { method: 'POST' });
416
+ console.log('Daemon shutdown requested');
417
+ } catch {
418
+ console.log('Daemon is not responding (may already be stopped)');
419
+ }
420
+ }
421
+ );
422
+
423
+ // Config command (standalone)
211
424
  cli.command(
212
425
  'config',
213
426
  'Save default settings for quest-dev commands',
@@ -242,39 +455,96 @@ cli.command(
242
455
  return;
243
456
  }
244
457
 
245
- const values: Record<string, unknown> = {};
246
- if (argv.pin !== undefined) values.pin = argv.pin;
247
- if (argv.idleTimeout !== undefined) values.idleTimeout = argv.idleTimeout;
248
- if (argv.lowBattery !== undefined) values.lowBattery = argv.lowBattery;
458
+ const values: QuestDevConfig = {};
459
+ if (argv.pin !== undefined) values.pin = argv.pin as string;
460
+ if (argv.port !== undefined) values.port = argv.port as number;
461
+ if (argv.device !== undefined) values.device = argv.device as string;
462
+ if (argv.idleTimeout !== undefined) values.idleTimeout = argv.idleTimeout as number;
463
+ if (argv.lowBattery !== undefined) values.lowBattery = argv.lowBattery as number;
249
464
 
250
465
  if (Object.keys(values).length === 0) {
251
- console.error('No config values provided. Use --pin, --idle-timeout, or --low-battery.');
466
+ console.error('No config values provided. Use --pin, --port, --device, --idle-timeout, or --low-battery.');
252
467
  process.exit(1);
253
468
  }
254
469
 
255
- saveConfig(values as any);
470
+ saveConfig(values);
256
471
  console.log('Config saved:');
257
472
  console.log(JSON.stringify(values, null, 2));
473
+
474
+ // Warn if device changed while daemon is running
475
+ if (values.device !== undefined && discoverDaemon()) {
476
+ console.log('\nNote: daemon is running. Restart it to use the new device:');
477
+ console.log(' quest-dev stop && quest-dev start');
478
+ }
258
479
  }
259
480
  );
260
481
 
261
- // Stay-awake watchdog (internal subcommand, spawned by stay-awake parent)
482
+ // Setup cast extract casting APK from MQDH
262
483
  cli.command(
263
- 'stay-awake-watchdog',
264
- false as any, // Hide from help
484
+ 'setup-cast [source]',
485
+ 'Extract casting APK from Meta Quest Developer Hub',
265
486
  (yargs) => {
266
487
  return yargs
267
- .option('parent-pid', {
268
- type: 'number',
269
- demandOption: true,
270
- })
271
- .option('pin', {
488
+ .positional('source', {
489
+ describe: 'Path to MQDH (.app, .dmg, .exe.zip, .exe, or directory). Omit to auto-detect.',
272
490
  type: 'string',
273
- demandOption: true,
274
- });
491
+ })
492
+ .example('$0 setup-cast', 'Auto-detect MQDH installation')
493
+ .example('$0 setup-cast "/Applications/Meta Quest Developer Hub.app"', 'macOS .app')
494
+ .example('$0 setup-cast ~/Downloads/MetaQuestDeveloperHub.dmg', 'macOS .dmg')
495
+ .example('$0 setup-cast ~/Downloads/Meta-Quest-Developer-Hub.exe.zip', 'Windows installer');
496
+ },
497
+ async (argv) => {
498
+ // If APKs already extracted, just confirm
499
+ if (!argv.source && hasCastingApk()) {
500
+ console.log('Casting APKs already extracted. Ready to cast.');
501
+ console.log('(Run with a path argument to re-extract from a newer MQDH version.)');
502
+ return;
503
+ }
504
+
505
+ let source = argv.source as string | undefined;
506
+
507
+ // Auto-detect if no source provided
508
+ if (!source) {
509
+ const found = findInstalledMqdh();
510
+ if (found) {
511
+ console.log(`Found MQDH: ${found}`);
512
+ source = found;
513
+ } else {
514
+ console.log('Could not find Meta Quest Developer Hub on this machine.\n');
515
+ console.log('Download it from:');
516
+ console.log(' https://developer.oculus.com/meta-quest-developer-hub\n');
517
+ console.log('Then run:');
518
+ console.log(' quest-dev setup-cast /path/to/Meta\\ Quest\\ Developer\\ Hub.app (macOS)');
519
+ console.log(' quest-dev setup-cast /path/to/MetaQuestDeveloperHub.dmg (macOS)');
520
+ console.log(' quest-dev setup-cast /path/to/Meta-Quest-Developer-Hub.exe.zip (Windows)');
521
+ process.exit(1);
522
+ }
523
+ }
524
+
525
+ console.log('Extracting casting APKs...');
526
+ await extractCastingApk(resolve(source));
527
+ console.log('\nDone. The APK will be auto-installed on your Quest when you start casting.');
528
+ }
529
+ );
530
+
531
+ // Hidden daemon subcommand (spawned by client.ts)
532
+ cli.command(
533
+ 'daemon',
534
+ false as any, // Hide from help
535
+ (yargs) => {
536
+ return yargs
537
+ .option('idle-timeout', { type: 'number' })
538
+ .option('low-battery', { type: 'number' });
275
539
  },
276
540
  async (argv) => {
277
- await stayAwakeWatchdog(argv.parentPid as number, argv.pin as string);
541
+ await startDaemon({
542
+ port: resolvePort(argv.port as number | undefined),
543
+ device: argv.device as string | undefined,
544
+ host: argv.host as string | undefined,
545
+ idleTimeout: argv.idleTimeout as number | undefined,
546
+ lowBattery: argv.lowBattery as number | undefined,
547
+ });
278
548
  }
279
549
  );
280
550