@myerscarpenter/quest-dev 1.4.0 → 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 (151) 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/open.d.ts.map +1 -1
  17. package/build/commands/open.js +9 -4
  18. package/build/commands/open.js.map +1 -1
  19. package/build/commands/screenshot.d.ts.map +1 -1
  20. package/build/commands/screenshot.js +17 -20
  21. package/build/commands/screenshot.js.map +1 -1
  22. package/build/commands/stay-awake.d.ts +2 -15
  23. package/build/commands/stay-awake.d.ts.map +1 -1
  24. package/build/commands/stay-awake.js +14 -77
  25. package/build/commands/stay-awake.js.map +1 -1
  26. package/build/daemon/cast-manager.d.ts +42 -0
  27. package/build/daemon/cast-manager.d.ts.map +1 -0
  28. package/build/daemon/cast-manager.js +243 -0
  29. package/build/daemon/cast-manager.js.map +1 -0
  30. package/build/daemon/client.d.ts +40 -0
  31. package/build/daemon/client.d.ts.map +1 -0
  32. package/build/daemon/client.js +133 -0
  33. package/build/daemon/client.js.map +1 -0
  34. package/build/daemon/daemon.d.ts +20 -0
  35. package/build/daemon/daemon.d.ts.map +1 -0
  36. package/build/daemon/daemon.js +130 -0
  37. package/build/daemon/daemon.js.map +1 -0
  38. package/build/daemon/deploy.d.ts +44 -0
  39. package/build/daemon/deploy.d.ts.map +1 -0
  40. package/build/daemon/deploy.js +230 -0
  41. package/build/daemon/deploy.js.map +1 -0
  42. package/build/daemon/logcat-manager.d.ts +39 -0
  43. package/build/daemon/logcat-manager.d.ts.map +1 -0
  44. package/build/daemon/logcat-manager.js +194 -0
  45. package/build/daemon/logcat-manager.js.map +1 -0
  46. package/build/daemon/server.d.ts +19 -0
  47. package/build/daemon/server.d.ts.map +1 -0
  48. package/build/daemon/server.js +482 -0
  49. package/build/daemon/server.js.map +1 -0
  50. package/build/daemon/stay-awake-manager.d.ts +22 -0
  51. package/build/daemon/stay-awake-manager.d.ts.map +1 -0
  52. package/build/daemon/stay-awake-manager.js +74 -0
  53. package/build/daemon/stay-awake-manager.js.map +1 -0
  54. package/build/index.js +285 -45
  55. package/build/index.js.map +1 -1
  56. package/build/public/dashboard.js +749 -0
  57. package/build/public/index.html +12 -0
  58. package/build/public/style.css +106 -0
  59. package/build/utils/adb.d.ts +12 -0
  60. package/build/utils/adb.d.ts.map +1 -1
  61. package/build/utils/adb.js +116 -51
  62. package/build/utils/adb.js.map +1 -1
  63. package/build/utils/casting-apk.d.ts +40 -0
  64. package/build/utils/casting-apk.d.ts.map +1 -0
  65. package/build/utils/casting-apk.js +252 -0
  66. package/build/utils/casting-apk.js.map +1 -0
  67. package/build/utils/config.d.ts +5 -3
  68. package/build/utils/config.d.ts.map +1 -1
  69. package/build/utils/config.js +18 -38
  70. package/build/utils/config.js.map +1 -1
  71. package/build/utils/exec.d.ts +5 -0
  72. package/build/utils/exec.d.ts.map +1 -1
  73. package/build/utils/exec.js +17 -0
  74. package/build/utils/exec.js.map +1 -1
  75. package/build/utils/filename.d.ts +7 -1
  76. package/build/utils/filename.d.ts.map +1 -1
  77. package/build/utils/filename.js +17 -2
  78. package/build/utils/filename.js.map +1 -1
  79. package/build/utils/filename.test.js +33 -1
  80. package/build/utils/filename.test.js.map +1 -1
  81. package/build/utils/jpeg-comment.d.ts +14 -0
  82. package/build/utils/jpeg-comment.d.ts.map +1 -0
  83. package/build/utils/jpeg-comment.js +28 -0
  84. package/build/utils/jpeg-comment.js.map +1 -0
  85. package/build/utils/test-properties.d.ts +34 -0
  86. package/build/utils/test-properties.d.ts.map +1 -0
  87. package/build/utils/test-properties.js +73 -0
  88. package/build/utils/test-properties.js.map +1 -0
  89. package/build/utils/verbose.d.ts +3 -0
  90. package/build/utils/verbose.d.ts.map +1 -0
  91. package/build/utils/verbose.js +13 -0
  92. package/build/utils/verbose.js.map +1 -0
  93. package/package.json +11 -5
  94. package/packages/cast2-protocol/README.md +86 -0
  95. package/packages/cast2-protocol/docs/_config.yml +4 -0
  96. package/packages/cast2-protocol/docs/feature-flags.md +102 -0
  97. package/packages/cast2-protocol/docs/index.md +24 -0
  98. package/packages/cast2-protocol/docs/open-investigations.md +149 -0
  99. package/packages/cast2-protocol/docs/protocol.md +602 -0
  100. package/packages/cast2-protocol/package.json +46 -0
  101. package/packages/cast2-protocol/src/constants.ts +65 -0
  102. package/packages/cast2-protocol/src/index.ts +7 -0
  103. package/packages/cast2-protocol/src/mgik.ts +69 -0
  104. package/packages/cast2-protocol/src/mud.ts +294 -0
  105. package/packages/cast2-protocol/src/pose.ts +99 -0
  106. package/packages/cast2-protocol/src/resolutions.ts +34 -0
  107. package/packages/cast2-protocol/src/types.ts +64 -0
  108. package/packages/cast2-protocol/src/xrsp.ts +73 -0
  109. package/packages/cast2-protocol/tests/mgik.test.ts +80 -0
  110. package/packages/cast2-protocol/tests/mud.test.ts +295 -0
  111. package/packages/cast2-protocol/tests/pose.test.ts +173 -0
  112. package/packages/cast2-protocol/tests/xrsp.test.ts +90 -0
  113. package/packages/cast2-protocol/tsconfig.json +20 -0
  114. package/pnpm-workspace.yaml +2 -0
  115. package/src/cast/decoder.ts +178 -0
  116. package/src/cast/session.ts +708 -0
  117. package/src/commands/logcat.ts +6 -5
  118. package/src/commands/open.ts +10 -3
  119. package/src/commands/screenshot.ts +19 -13
  120. package/src/commands/stay-awake.ts +22 -91
  121. package/src/daemon/adbkit-apkreader.d.ts +14 -0
  122. package/src/daemon/cast-manager.ts +282 -0
  123. package/src/daemon/client.ts +166 -0
  124. package/src/daemon/daemon.ts +169 -0
  125. package/src/daemon/deploy.ts +307 -0
  126. package/src/daemon/logcat-manager.ts +229 -0
  127. package/src/daemon/server.ts +595 -0
  128. package/src/daemon/stay-awake-manager.ts +83 -0
  129. package/src/index.ts +340 -56
  130. package/src/public/dashboard.js +288 -0
  131. package/src/public/index.html +12 -0
  132. package/src/public/style.css +106 -0
  133. package/src/utils/adb.ts +129 -42
  134. package/src/utils/casting-apk.ts +276 -0
  135. package/src/utils/config.ts +18 -36
  136. package/src/utils/exec.ts +20 -0
  137. package/src/utils/filename.test.ts +41 -1
  138. package/src/utils/filename.ts +18 -2
  139. package/src/utils/jpeg-comment.ts +30 -0
  140. package/src/utils/test-properties.ts +94 -0
  141. package/src/utils/verbose.ts +14 -0
  142. package/tests/cast/auto-layer.test.ts +87 -0
  143. package/tests/cast/decoder.test.ts +82 -0
  144. package/tests/cast/session-restart.test.ts +107 -0
  145. package/tests/config.test.ts +17 -22
  146. package/tests/daemon/api-status.test.ts +82 -0
  147. package/tests/daemon/cast-manager.test.ts +69 -0
  148. package/tests/daemon/mjpeg-stream.test.ts +144 -0
  149. package/tests/daemon/pose-endpoint.test.ts +63 -0
  150. package/tests/daemon/start-guard.test.ts +77 -0
  151. package/vitest.config.ts +10 -0
package/src/index.ts CHANGED
@@ -9,13 +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
+ 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';
19
23
 
20
24
  // Read version from package.json
21
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -31,6 +35,27 @@ const cli = yargs(hideBin(process.argv))
31
35
  .usage('Usage: $0 <command> [options]')
32
36
  .demandCommand(1, '')
33
37
  .strict()
38
+ .option('verbose', {
39
+ describe: 'Show detailed debug output',
40
+ type: 'boolean',
41
+ default: false,
42
+ global: true,
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
+ })
34
59
  .fail((msg, err, yargs) => {
35
60
  yargs.showHelp();
36
61
  if (err) console.error(err.message);
@@ -38,9 +63,9 @@ const cli = yargs(hideBin(process.argv))
38
63
  })
39
64
  .help()
40
65
  .alias('help', 'h')
41
- .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).');
42
67
 
43
- // Screenshot command
68
+ // Screenshot command (standalone — no daemon needed)
44
69
  cli.command(
45
70
  'screenshot <directory>',
46
71
  'Take a screenshot from Quest and save to directory with auto-generated filename',
@@ -65,7 +90,7 @@ cli.command(
65
90
  }
66
91
  );
67
92
 
68
- // Open command
93
+ // Open command (standalone)
69
94
  cli.command(
70
95
  'open <url>',
71
96
  'Open URL in Quest browser (sets up CDP debugging port forwarding)',
@@ -97,7 +122,7 @@ cli.command(
97
122
  }
98
123
  );
99
124
 
100
- // Logcat command
125
+ // Logcat command — delegates to daemon for start/stop/status, tail remains standalone
101
126
  cli.command(
102
127
  'logcat <action>',
103
128
  'Capture Android logcat to files (CRITICAL: always start before testing to avoid losing crash logs)',
@@ -116,29 +141,73 @@ cli.command(
116
141
  },
117
142
  async (argv) => {
118
143
  const action = argv.action as string;
119
- const filter = argv.filter as string | undefined;
120
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 });
121
152
  switch (action) {
122
- case 'start':
123
- await startCommand(filter);
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
+ }
124
166
  break;
125
- case 'stop':
126
- await stopCommand();
127
- break;
128
- case 'status':
129
- 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
+ }
130
185
  break;
131
- case 'tail':
132
- 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
+ }
133
204
  break;
134
- default:
135
- console.error(`Unknown action: ${action}`);
136
- process.exit(1);
205
+ }
137
206
  }
138
207
  }
139
208
  );
140
209
 
141
- // Battery command
210
+ // Battery command (standalone)
142
211
  cli.command(
143
212
  'battery',
144
213
  'Show Quest battery percentage and charging status',
@@ -148,10 +217,42 @@ cli.command(
148
217
  }
149
218
  );
150
219
 
151
- // 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
152
253
  cli.command(
153
254
  'stay-awake',
154
- 'Keep Quest awake (disables autosleep, guardian, dialogs)',
255
+ 'Keep Quest awake (disables autosleep, guardian, dialogs) via daemon',
155
256
  (yargs) => {
156
257
  return yargs
157
258
  .option('pin', {
@@ -177,30 +278,149 @@ cli.command(
177
278
  type: 'boolean',
178
279
  default: false,
179
280
  })
180
- .option('verbose', {
181
- describe: 'Print battery level on every check (every 60s)',
182
- type: 'boolean',
183
- default: false,
184
- alias: 'v',
185
- });
281
+ ;
186
282
  },
187
283
  async (argv) => {
188
284
  if (argv.status) {
189
285
  await stayAwakeStatus();
190
- } else if (argv.disable) {
191
- 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}`);
192
314
  } else {
193
- await stayAwakeCommand(
194
- argv.pin as string | undefined,
195
- argv.idleTimeout as number | undefined,
196
- argv.lowBattery as number | undefined,
197
- argv.verbose as boolean,
198
- );
315
+ console.error('Failed to enable stay-awake:', result.error);
316
+ process.exit(1);
199
317
  }
200
318
  }
201
319
  );
202
320
 
203
- // 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)
204
424
  cli.command(
205
425
  'config',
206
426
  'Save default settings for quest-dev commands',
@@ -235,41 +455,105 @@ cli.command(
235
455
  return;
236
456
  }
237
457
 
238
- const values: Record<string, unknown> = {};
239
- if (argv.pin !== undefined) values.pin = argv.pin;
240
- if (argv.idleTimeout !== undefined) values.idleTimeout = argv.idleTimeout;
241
- 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;
242
464
 
243
465
  if (Object.keys(values).length === 0) {
244
- 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.');
245
467
  process.exit(1);
246
468
  }
247
469
 
248
- saveConfig(values as any);
470
+ saveConfig(values);
249
471
  console.log('Config saved:');
250
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
+ }
251
479
  }
252
480
  );
253
481
 
254
- // Stay-awake watchdog (internal subcommand, spawned by stay-awake parent)
482
+ // Setup cast extract casting APK from MQDH
255
483
  cli.command(
256
- 'stay-awake-watchdog',
257
- false as any, // Hide from help
484
+ 'setup-cast [source]',
485
+ 'Extract casting APK from Meta Quest Developer Hub',
258
486
  (yargs) => {
259
487
  return yargs
260
- .option('parent-pid', {
261
- type: 'number',
262
- demandOption: true,
263
- })
264
- .option('pin', {
488
+ .positional('source', {
489
+ describe: 'Path to MQDH (.app, .dmg, .exe.zip, .exe, or directory). Omit to auto-detect.',
265
490
  type: 'string',
266
- demandOption: true,
267
- });
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' });
268
539
  },
269
540
  async (argv) => {
270
- 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
+ });
271
548
  }
272
549
  );
273
550
 
551
+ // Set verbose flag before any command runs
552
+ cli.middleware((argv) => {
553
+ if (argv.verbose) {
554
+ setVerbose(true);
555
+ }
556
+ });
557
+
274
558
  // Parse and execute
275
559
  cli.parse();