@luxonis/depthai-viewer-common 2.5.2 → 2.5.3

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.
@@ -0,0 +1,1071 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as prompts from '@clack/prompts';
4
+ import { spawn } from 'node:child_process';
5
+ import { existsSync, statSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import process from 'node:process';
8
+ import { clearLine, cursorTo } from 'node:readline';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const repoRoot = join(dirname(__filename), '..', '..');
13
+
14
+ const benchmarkCatalog = [
15
+ { id: 'bgr888i-stream', label: 'BGR888i Stream' },
16
+ { id: 'encoded-stream', label: 'H.264 Stream' },
17
+ { id: 'nv12-stream', label: 'NV12 Stream' },
18
+ { id: 'pointcloud', label: 'Point Cloud' },
19
+ ];
20
+ const backValue = '__back__';
21
+ const customResolutionValue = '__custom_resolution__';
22
+ const sourceResolutionValue = '__source_resolution__';
23
+ const defaultResolution = process.env.BENCHMARK_RESOLUTION ?? '1920x1080';
24
+ const progressEventPrefix = '__BENCHMARK_PROGRESS__|';
25
+
26
+ const benchmarkById = new Map(benchmarkCatalog.map((benchmark) => [benchmark.id, benchmark]));
27
+ const rootRunCommand = ['bash', './packages/common/benchmark-root.sh', '--'];
28
+ const rootBenchmarkScript = ['npm', 'run', '-w=@luxonis/depthai-viewer-common', 'benchmark:raw', '--'];
29
+ const signalExitCodes = {
30
+ SIGINT: 130,
31
+ SIGTERM: 143,
32
+ };
33
+
34
+ function printHelp() {
35
+ process.stdout.write(`DepthAI benchmark CLI
36
+
37
+ Usage:
38
+ node ./packages/common/benchmark-cli.mjs [command] [options]
39
+
40
+ Commands:
41
+ run Run the full benchmark flow with visualizer startup.
42
+ download Download benchmark assets only.
43
+ list Show available benchmarks.
44
+
45
+ Options:
46
+ --benchmark ID Select a single benchmark. Can be repeated.
47
+ --benchmarks ID1,ID2 Select a comma-separated list of benchmarks.
48
+ --resolution WIDTHxHEIGHT Resolution used by the run command.
49
+ --interactive Force interactive benchmark selection.
50
+ --no-interactive Disable interactive prompts.
51
+ -h, --help Show this help message.
52
+
53
+ Examples:
54
+ npm run benchmark
55
+ npm run benchmark -- download
56
+ npm run benchmark -- run --benchmark encoded-stream
57
+ npm run benchmark -- list
58
+ `);
59
+ }
60
+
61
+ function parseArgs(argv) {
62
+ const args = [...argv];
63
+ let command = null;
64
+
65
+ const options = {
66
+ benchmarkIds: [],
67
+ resolution: defaultResolution,
68
+ hasResolutionFlag: false,
69
+ interactive: null,
70
+ help: false,
71
+ };
72
+
73
+ while (args.length > 0) {
74
+ const current = args.shift();
75
+
76
+ switch (current) {
77
+ case '--benchmark': {
78
+ const value = args.shift();
79
+ if (!value) {
80
+ throw new Error('Missing value for --benchmark');
81
+ }
82
+ options.benchmarkIds.push(value);
83
+ break;
84
+ }
85
+ case '--benchmarks': {
86
+ const value = args.shift();
87
+ if (!value) {
88
+ throw new Error('Missing value for --benchmarks');
89
+ }
90
+ for (const benchmarkId of value.split(',')) {
91
+ const trimmed = benchmarkId.trim();
92
+ if (trimmed) {
93
+ options.benchmarkIds.push(trimmed);
94
+ }
95
+ }
96
+ break;
97
+ }
98
+ case '--resolution': {
99
+ const value = args.shift();
100
+ if (!value) {
101
+ throw new Error('Missing value for --resolution');
102
+ }
103
+ options.resolution = value;
104
+ options.hasResolutionFlag = true;
105
+ break;
106
+ }
107
+ case '--interactive':
108
+ options.interactive = true;
109
+ break;
110
+ case '--no-interactive':
111
+ options.interactive = false;
112
+ break;
113
+ case '-h':
114
+ case '--help':
115
+ case 'help':
116
+ options.help = true;
117
+ break;
118
+ case 'run':
119
+ case 'download':
120
+ case 'list':
121
+ if (command !== null) {
122
+ throw new Error(`Unexpected command: ${current}`);
123
+ }
124
+ command = current;
125
+ break;
126
+ default:
127
+ if (!current.startsWith('-') && command === null) {
128
+ command = current;
129
+ break;
130
+ }
131
+ throw new Error(`Unknown argument: ${current}`);
132
+ }
133
+ }
134
+
135
+ options.benchmarkIds = [...new Set(options.benchmarkIds)];
136
+ return { command, options };
137
+ }
138
+
139
+ function validateBenchmarkIds(benchmarkIds) {
140
+ for (const benchmarkId of benchmarkIds) {
141
+ if (!benchmarkById.has(benchmarkId)) {
142
+ throw new Error(`Unknown benchmark: ${benchmarkId}`);
143
+ }
144
+ }
145
+ }
146
+
147
+ function validateResolution(resolution) {
148
+ if (!/^[0-9]+x[0-9]+$/.test(resolution)) {
149
+ throw new Error('Resolution must be in WIDTHxHEIGHT format');
150
+ }
151
+
152
+ const [widthText, heightText] = resolution.split('x');
153
+ const width = Number(widthText);
154
+ const height = Number(heightText);
155
+
156
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
157
+ throw new Error('Resolution width and height must be positive integers');
158
+ }
159
+
160
+ if (width % 2 !== 0 || height % 2 !== 0) {
161
+ throw new Error('Resolution width and height must be even');
162
+ }
163
+ }
164
+
165
+ function shouldPromptForAction(options, command) {
166
+ if (options.interactive === false || command !== null) {
167
+ return false;
168
+ }
169
+
170
+ return process.stdin.isTTY && process.stdout.isTTY;
171
+ }
172
+
173
+ function shouldPromptForBenchmarks(options) {
174
+ if (options.interactive === false || options.benchmarkIds.length > 0) {
175
+ return false;
176
+ }
177
+
178
+ return process.stdin.isTTY && process.stdout.isTTY;
179
+ }
180
+
181
+ function shouldPromptForResolution(options, command) {
182
+ if (command !== 'run') {
183
+ return false;
184
+ }
185
+
186
+ if (options.interactive === false || options.hasResolutionFlag) {
187
+ return false;
188
+ }
189
+
190
+ return process.stdin.isTTY && process.stdout.isTTY;
191
+ }
192
+
193
+ function toSelectionArgs(benchmarkIds) {
194
+ const args = [];
195
+
196
+ for (const benchmarkId of benchmarkIds) {
197
+ args.push('--benchmark', benchmarkId);
198
+ }
199
+
200
+ return args;
201
+ }
202
+
203
+ async function promptForAction() {
204
+ const action = await prompts.select({
205
+ message: 'What do you want to do?',
206
+ options: [
207
+ {
208
+ value: 'run',
209
+ label: 'Run benchmarks',
210
+ hint: 'if benchmarks are not downloaded yet, they will be downloaded first',
211
+ },
212
+ {
213
+ value: 'download',
214
+ label: 'Download benchmarks',
215
+ hint: 'only download benchmarks without running them',
216
+ },
217
+ ],
218
+ });
219
+
220
+ return prompts.isCancel(action) ? null : action;
221
+ }
222
+
223
+ async function promptForBenchmarks() {
224
+ const selectionMode = await prompts.select({
225
+ message: 'Which benchmarks do you want to use?',
226
+ options: [
227
+ { value: 'all', label: 'All benchmarks' },
228
+ { value: 'single', label: 'Choose one benchmark' },
229
+ { value: 'multiple', label: 'Choose multiple benchmarks' },
230
+ { value: backValue, label: 'Back to main menu' },
231
+ ],
232
+ });
233
+
234
+ if (prompts.isCancel(selectionMode)) {
235
+ return null;
236
+ }
237
+
238
+ if (selectionMode === backValue) {
239
+ return backValue;
240
+ }
241
+
242
+ if (selectionMode === 'all') {
243
+ return benchmarkCatalog.map((benchmark) => benchmark.id);
244
+ }
245
+
246
+ if (selectionMode === 'single') {
247
+ const benchmarkId = await prompts.select({
248
+ message: 'Pick a benchmark',
249
+ options: benchmarkCatalog
250
+ .map((benchmark) => ({
251
+ value: benchmark.id,
252
+ label: benchmark.label,
253
+ hint: benchmark.id,
254
+ }))
255
+ .concat([{ value: backValue, label: 'Back to benchmark mode' }]),
256
+ });
257
+
258
+ if (prompts.isCancel(benchmarkId)) {
259
+ return null;
260
+ }
261
+
262
+ return benchmarkId === backValue ? backValue : [benchmarkId];
263
+ }
264
+
265
+ const benchmarkIds = await prompts.multiselect({
266
+ message: 'Select one or more benchmarks',
267
+ required: true,
268
+ options: benchmarkCatalog.map((benchmark) => ({
269
+ value: benchmark.id,
270
+ label: benchmark.label,
271
+ hint: benchmark.id,
272
+ })),
273
+ });
274
+
275
+ return prompts.isCancel(benchmarkIds) ? null : benchmarkIds;
276
+ }
277
+
278
+ async function promptForResolution(currentResolution, benchmarkIds) {
279
+ for (;;) {
280
+ const selectedResolution = await prompts.select({
281
+ message: 'Which resolution should the benchmarks use?',
282
+ options: [
283
+ {
284
+ value: '1920x1080',
285
+ label: '1920x1080',
286
+ hint: currentResolution === '1920x1080' ? 'current default' : 'Full HD',
287
+ },
288
+ {
289
+ value: '1200x800',
290
+ label: '1200x800',
291
+ hint: 'HD',
292
+ },
293
+ {
294
+ value: '3840x2160',
295
+ label: '3840x2160',
296
+ hint: '4K',
297
+ },
298
+ {
299
+ value: sourceResolutionValue,
300
+ label: 'Recorded source resolution',
301
+ hint: 'run without resizing',
302
+ },
303
+ {
304
+ value: customResolutionValue,
305
+ label: 'Custom resolution',
306
+ hint: 'enter WIDTHxHEIGHT',
307
+ },
308
+ {
309
+ value: backValue,
310
+ label: 'Back to benchmark selection',
311
+ },
312
+ ],
313
+ });
314
+
315
+ if (prompts.isCancel(selectedResolution)) {
316
+ return null;
317
+ }
318
+
319
+ if (selectedResolution === backValue) {
320
+ return backValue;
321
+ }
322
+
323
+ if (selectedResolution === sourceResolutionValue) {
324
+ return sourceResolutionValue;
325
+ }
326
+
327
+ if (selectedResolution === customResolutionValue) {
328
+ const customResolution = await prompts.text({
329
+ message: 'Enter a resolution in WIDTHxHEIGHT format',
330
+ placeholder: currentResolution,
331
+ validate: (value) => {
332
+ try {
333
+ validateResolution(value);
334
+ return;
335
+ } catch (error) {
336
+ return error instanceof Error ? error.message : String(error);
337
+ }
338
+ },
339
+ });
340
+
341
+ if (prompts.isCancel(customResolution)) {
342
+ return null;
343
+ }
344
+
345
+ return customResolution;
346
+ }
347
+
348
+ return selectedResolution;
349
+ }
350
+ }
351
+
352
+ function attachSignalForwarding(child, onSignal) {
353
+ const signals = ['SIGINT', 'SIGTERM'];
354
+ let forwardedSignal = null;
355
+ const handlers = new Map();
356
+
357
+ for (const signal of signals) {
358
+ const handler = () => {
359
+ if (forwardedSignal !== null) {
360
+ return;
361
+ }
362
+
363
+ forwardedSignal = signal;
364
+ onSignal?.(signal);
365
+ if (!child.killed) {
366
+ child.kill(signal);
367
+ }
368
+ };
369
+
370
+ handlers.set(signal, handler);
371
+ process.on(signal, handler);
372
+ }
373
+
374
+ return {
375
+ get forwardedSignal() {
376
+ return forwardedSignal;
377
+ },
378
+ cleanup() {
379
+ for (const [signal, handler] of handlers) {
380
+ process.off(signal, handler);
381
+ }
382
+ },
383
+ };
384
+ }
385
+
386
+ async function runCommand(command, args, { cwd = repoRoot, env = process.env } = {}) {
387
+ await new Promise((resolve, reject) => {
388
+ const child = spawn(command, args, {
389
+ cwd,
390
+ env,
391
+ stdio: 'inherit',
392
+ });
393
+ const signalForwarding = attachSignalForwarding(child);
394
+
395
+ child.on('error', (error) => {
396
+ signalForwarding.cleanup();
397
+ reject(error);
398
+ });
399
+
400
+ child.on('close', (code, signal) => {
401
+ const forwardedSignal = signalForwarding.forwardedSignal ?? signal;
402
+ signalForwarding.cleanup();
403
+
404
+ if (forwardedSignal) {
405
+ process.exit(signalExitCodes[forwardedSignal] ?? 1);
406
+ }
407
+
408
+ if (code === 0) {
409
+ resolve();
410
+ return;
411
+ }
412
+
413
+ reject(new Error(`${command} exited with code ${code ?? 1}`));
414
+ });
415
+ });
416
+ }
417
+
418
+ async function runTaskWithSpinner(message, command, args, { cwd = repoRoot, env = process.env } = {}) {
419
+ const taskSpinner = prompts.spinner();
420
+ taskSpinner.start(message);
421
+
422
+ await new Promise((resolve, reject) => {
423
+ const child = spawn(command, args, {
424
+ cwd,
425
+ env,
426
+ stdio: ['inherit', 'pipe', 'pipe'],
427
+ });
428
+ let stdout = '';
429
+ let stderr = '';
430
+ const signalForwarding = attachSignalForwarding(child, () => {
431
+ taskSpinner.stop(`${message} canceled`);
432
+ });
433
+
434
+ child.stdout.on('data', (chunk) => {
435
+ stdout += chunk.toString();
436
+ });
437
+
438
+ child.stderr.on('data', (chunk) => {
439
+ stderr += chunk.toString();
440
+ });
441
+
442
+ child.on('error', (error) => {
443
+ signalForwarding.cleanup();
444
+ taskSpinner.stop(`${message} failed`);
445
+ reject(error);
446
+ });
447
+
448
+ child.on('close', (code, signal) => {
449
+ const forwardedSignal = signalForwarding.forwardedSignal ?? signal;
450
+ signalForwarding.cleanup();
451
+
452
+ if (forwardedSignal) {
453
+ process.exit(signalExitCodes[forwardedSignal] ?? 1);
454
+ }
455
+
456
+ if (code === 0) {
457
+ taskSpinner.stop(message);
458
+ resolve();
459
+ return;
460
+ }
461
+
462
+ taskSpinner.stop(`${message} failed`);
463
+ if (stdout) {
464
+ process.stdout.write(stdout);
465
+ }
466
+ if (stderr) {
467
+ process.stderr.write(stderr);
468
+ }
469
+ reject(new Error(`${command} exited with code ${code ?? 1}`));
470
+ });
471
+ });
472
+ }
473
+
474
+ function formatDownloadStatus(line) {
475
+ const normalizedLine = line.startsWith('[benchmark] ') ? line.slice(12) : line;
476
+
477
+ if (normalizedLine.startsWith('Installing Python dependencies from ')) {
478
+ return 'Installing Python dependencies';
479
+ }
480
+
481
+ if (normalizedLine.startsWith('Downloading ')) {
482
+ return normalizedLine;
483
+ }
484
+
485
+ if (normalizedLine.startsWith('Resuming ')) {
486
+ return normalizedLine;
487
+ }
488
+
489
+ if (normalizedLine.startsWith('Using existing asset ')) {
490
+ return normalizedLine;
491
+ }
492
+
493
+ if (normalizedLine.startsWith('Recovered completed asset ')) {
494
+ return normalizedLine;
495
+ }
496
+
497
+ if (normalizedLine.startsWith('Download completed: ')) {
498
+ return normalizedLine;
499
+ }
500
+
501
+ if (normalizedLine.startsWith('Retrying ')) {
502
+ return normalizedLine;
503
+ }
504
+
505
+ return normalizedLine;
506
+ }
507
+
508
+ function formatBytes(bytes) {
509
+ if (!Number.isFinite(bytes) || bytes < 0) {
510
+ return '0 B';
511
+ }
512
+
513
+ if (bytes < 1024) {
514
+ return `${bytes} B`;
515
+ }
516
+
517
+ const units = ['KB', 'MB', 'GB', 'TB'];
518
+ let value = bytes;
519
+ let unitIndex = -1;
520
+
521
+ while (value >= 1024 && unitIndex < units.length - 1) {
522
+ value /= 1024;
523
+ unitIndex += 1;
524
+ }
525
+
526
+ return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
527
+ }
528
+
529
+ function fitToTerminalWidth(text) {
530
+ const columns = process.stdout.columns ?? 120;
531
+ const maxWidth = Math.max(20, columns - 1);
532
+
533
+ if (text.length <= maxWidth) {
534
+ return text;
535
+ }
536
+
537
+ if (maxWidth <= 3) {
538
+ return text.slice(0, maxWidth);
539
+ }
540
+
541
+ return `${text.slice(0, maxWidth - 3)}...`;
542
+ }
543
+
544
+ function formatProgressLabel(progressState) {
545
+ const baseLabel = `${progressState.mode === 'resume' ? 'Resuming' : 'Downloading'} ${progressState.fileName} for ${progressState.benchmarkId}`;
546
+
547
+ if (!progressState.expectedSize || progressState.expectedSize <= 0) {
548
+ return baseLabel;
549
+ }
550
+
551
+ const percent = Math.max(
552
+ 0,
553
+ Math.min(100, Math.floor((progressState.currentSize / progressState.expectedSize) * 100)),
554
+ );
555
+ const downloadedSize = Math.min(progressState.currentSize, progressState.expectedSize);
556
+ const remainingSize = Math.max(0, progressState.expectedSize - downloadedSize);
557
+ return `${baseLabel} (${percent}%) • ${formatBytes(downloadedSize)} / ${formatBytes(progressState.expectedSize)} • ${formatBytes(remainingSize)} left`;
558
+ }
559
+
560
+ function createDownloadStatusHandler(taskSpinner) {
561
+ let progressTimer = null;
562
+ let progressState = null;
563
+
564
+ const clearProgressTimer = () => {
565
+ if (progressTimer !== null) {
566
+ clearInterval(progressTimer);
567
+ progressTimer = null;
568
+ }
569
+ };
570
+
571
+ const updateProgressMessage = () => {
572
+ if (progressState === null) {
573
+ return;
574
+ }
575
+
576
+ if (existsSync(progressState.filePath)) {
577
+ try {
578
+ progressState.currentSize = statSync(progressState.filePath).size;
579
+ } catch {
580
+ return;
581
+ }
582
+ }
583
+
584
+ taskSpinner.message(formatProgressLabel(progressState));
585
+ };
586
+
587
+ return {
588
+ cleanup() {
589
+ clearProgressTimer();
590
+ },
591
+ handleLine(line) {
592
+ if (line.startsWith(progressEventPrefix)) {
593
+ const [, action, benchmarkId, fileName, filePath, expectedSizeText, mode] =
594
+ line.split('|');
595
+
596
+ if (action === 'start') {
597
+ clearProgressTimer();
598
+ progressState = {
599
+ benchmarkId,
600
+ fileName,
601
+ filePath,
602
+ expectedSize: Number(expectedSizeText) || 0,
603
+ currentSize: 0,
604
+ mode,
605
+ };
606
+ updateProgressMessage();
607
+
608
+ if (progressState.expectedSize > 0) {
609
+ progressTimer = setInterval(updateProgressMessage, 200);
610
+ }
611
+
612
+ return { hidden: true };
613
+ }
614
+
615
+ if (action === 'finish') {
616
+ clearProgressTimer();
617
+ progressState = null;
618
+ return { hidden: true };
619
+ }
620
+ }
621
+
622
+ return {
623
+ hidden: false,
624
+ message: formatDownloadStatus(line),
625
+ };
626
+ },
627
+ };
628
+ }
629
+
630
+ async function runStreamingCommandWithSpinner(
631
+ message,
632
+ command,
633
+ args,
634
+ { cwd = repoRoot, env = process.env, createLineHandler, successMessage } = {},
635
+ ) {
636
+ if (!process.stdout.isTTY) {
637
+ const taskSpinner = prompts.spinner();
638
+ taskSpinner.start(message);
639
+
640
+ await new Promise((resolve, reject) => {
641
+ const child = spawn(command, args, {
642
+ cwd,
643
+ env,
644
+ stdio: ['inherit', 'pipe', 'pipe'],
645
+ });
646
+ const signalForwarding = attachSignalForwarding(child, () => {
647
+ taskSpinner.stop(`${message} canceled`);
648
+ });
649
+ let stdoutBuffer = '';
650
+ let stderrBuffer = '';
651
+ let stdoutOutput = '';
652
+ let stderrOutput = '';
653
+ const lineHandler = createLineHandler?.({
654
+ message() {},
655
+ });
656
+
657
+ const flushLines = (buffer, appendOutput) => {
658
+ const normalized = buffer.replaceAll('\r', '\n');
659
+ const parts = normalized.split('\n');
660
+ const remainder = parts.pop() ?? '';
661
+
662
+ for (const part of parts) {
663
+ const line = part.trim();
664
+ if (!line) {
665
+ continue;
666
+ }
667
+
668
+ const result = lineHandler?.handleLine(part);
669
+ if (!result?.hidden) {
670
+ appendOutput(`${part}\n`);
671
+ }
672
+ }
673
+
674
+ return remainder;
675
+ };
676
+
677
+ child.stdout.on('data', (chunk) => {
678
+ stdoutBuffer += chunk.toString();
679
+ stdoutBuffer = flushLines(stdoutBuffer, (value) => {
680
+ stdoutOutput += value;
681
+ });
682
+ });
683
+
684
+ child.stderr.on('data', (chunk) => {
685
+ stderrBuffer += chunk.toString();
686
+ stderrBuffer = flushLines(stderrBuffer, (value) => {
687
+ stderrOutput += value;
688
+ });
689
+ });
690
+
691
+ child.on('error', (error) => {
692
+ lineHandler?.cleanup?.();
693
+ signalForwarding.cleanup();
694
+ taskSpinner.stop(`${message} failed`);
695
+ reject(error);
696
+ });
697
+
698
+ child.on('close', (code, signal) => {
699
+ lineHandler?.cleanup?.();
700
+ const forwardedSignal = signalForwarding.forwardedSignal ?? signal;
701
+ signalForwarding.cleanup();
702
+ const remainingStdout = stdoutBuffer.trim();
703
+ const remainingStderr = stderrBuffer.trim();
704
+ if (remainingStdout) {
705
+ const result = lineHandler?.handleLine(remainingStdout);
706
+ if (!result?.hidden) {
707
+ stdoutOutput += `${remainingStdout}\n`;
708
+ }
709
+ }
710
+ if (remainingStderr) {
711
+ const result = lineHandler?.handleLine(remainingStderr);
712
+ if (!result?.hidden) {
713
+ stderrOutput += `${remainingStderr}\n`;
714
+ }
715
+ }
716
+
717
+ if (forwardedSignal) {
718
+ process.exit(signalExitCodes[forwardedSignal] ?? 1);
719
+ }
720
+
721
+ if (code === 0) {
722
+ taskSpinner.stop(successMessage ?? message);
723
+ resolve();
724
+ return;
725
+ }
726
+
727
+ taskSpinner.stop(`${message} failed`);
728
+ if (stdoutOutput) {
729
+ process.stdout.write(stdoutOutput);
730
+ }
731
+ if (stderrOutput) {
732
+ process.stderr.write(stderrOutput);
733
+ }
734
+ reject(new Error(`${command} exited with code ${code ?? 1}`));
735
+ });
736
+ });
737
+ return;
738
+ }
739
+
740
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
741
+ let spinnerIndex = 0;
742
+ let activeMessage = message;
743
+ let renderTimer = null;
744
+
745
+ const renderLine = () => {
746
+ const frame = spinnerFrames[spinnerIndex];
747
+ const text = fitToTerminalWidth(`${frame} ${activeMessage}`);
748
+ cursorTo(process.stdout, 0);
749
+ clearLine(process.stdout, 0);
750
+ process.stdout.write(text);
751
+ spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
752
+ };
753
+
754
+ const stopLine = (finalMessage) => {
755
+ if (renderTimer !== null) {
756
+ clearInterval(renderTimer);
757
+ renderTimer = null;
758
+ }
759
+
760
+ const text = fitToTerminalWidth(`◆ ${finalMessage}`);
761
+ cursorTo(process.stdout, 0);
762
+ clearLine(process.stdout, 0);
763
+ process.stdout.write(`${text}\n`);
764
+ };
765
+
766
+ renderLine();
767
+ renderTimer = setInterval(renderLine, 80);
768
+
769
+ await new Promise((resolve, reject) => {
770
+ const child = spawn(command, args, {
771
+ cwd,
772
+ env,
773
+ stdio: ['inherit', 'pipe', 'pipe'],
774
+ });
775
+ const signalForwarding = attachSignalForwarding(child, () => {
776
+ stopLine(`${message} canceled`);
777
+ });
778
+ let stdoutBuffer = '';
779
+ let stderrBuffer = '';
780
+ let stdoutOutput = '';
781
+ let stderrOutput = '';
782
+ const inlineSpinnerHandle = {
783
+ message(nextMessage) {
784
+ activeMessage = nextMessage;
785
+ renderLine();
786
+ },
787
+ };
788
+ const inlineLineHandler = createLineHandler?.(inlineSpinnerHandle);
789
+
790
+ const flushLines = (buffer, appendOutput) => {
791
+ const normalized = buffer.replaceAll('\r', '\n');
792
+ const parts = normalized.split('\n');
793
+ const remainder = parts.pop() ?? '';
794
+
795
+ for (const part of parts) {
796
+ const line = part.trim();
797
+ if (!line) {
798
+ continue;
799
+ }
800
+
801
+ const result = inlineLineHandler?.handleLine(part);
802
+ if (!result?.hidden) {
803
+ appendOutput(`${part}\n`);
804
+ }
805
+ }
806
+
807
+ return remainder;
808
+ };
809
+
810
+ child.stdout.on('data', (chunk) => {
811
+ stdoutBuffer += chunk.toString();
812
+ stdoutBuffer = flushLines(
813
+ stdoutBuffer,
814
+ (value) => {
815
+ stdoutOutput += value;
816
+ },
817
+ );
818
+ });
819
+
820
+ child.stderr.on('data', (chunk) => {
821
+ stderrBuffer += chunk.toString();
822
+ stderrBuffer = flushLines(
823
+ stderrBuffer,
824
+ (value) => {
825
+ stderrOutput += value;
826
+ },
827
+ );
828
+ });
829
+
830
+ child.on('error', (error) => {
831
+ inlineLineHandler?.cleanup?.();
832
+ signalForwarding.cleanup();
833
+ stopLine(`${message} failed`);
834
+ reject(error);
835
+ });
836
+
837
+ child.on('close', (code, signal) => {
838
+ inlineLineHandler?.cleanup?.();
839
+ const forwardedSignal = signalForwarding.forwardedSignal ?? signal;
840
+ signalForwarding.cleanup();
841
+ const remainingStdout = stdoutBuffer.trim();
842
+ const remainingStderr = stderrBuffer.trim();
843
+ if (remainingStdout) {
844
+ const result = inlineLineHandler?.handleLine(remainingStdout);
845
+ if (!result?.hidden) {
846
+ stdoutOutput += `${remainingStdout}\n`;
847
+ }
848
+ }
849
+ if (remainingStderr) {
850
+ const result = inlineLineHandler?.handleLine(remainingStderr);
851
+ if (!result?.hidden) {
852
+ stderrOutput += `${remainingStderr}\n`;
853
+ }
854
+ }
855
+
856
+ if (forwardedSignal) {
857
+ process.exit(signalExitCodes[forwardedSignal] ?? 1);
858
+ }
859
+
860
+ if (code === 0) {
861
+ stopLine(successMessage ?? message);
862
+ resolve();
863
+ return;
864
+ }
865
+
866
+ stopLine(`${message} failed`);
867
+ if (stdoutOutput) {
868
+ process.stdout.write(stdoutOutput);
869
+ }
870
+ if (stderrOutput) {
871
+ process.stderr.write(stderrOutput);
872
+ }
873
+ reject(new Error(`${command} exited with code ${code ?? 1}`));
874
+ });
875
+ });
876
+ }
877
+
878
+ async function resolveBenchmarkIds(options) {
879
+ validateBenchmarkIds(options.benchmarkIds);
880
+
881
+ if (!shouldPromptForBenchmarks(options)) {
882
+ return options.benchmarkIds;
883
+ }
884
+
885
+ const selectedBenchmarkIds = await promptForBenchmarks();
886
+ if (selectedBenchmarkIds === null) {
887
+ prompts.cancel('Benchmark selection cancelled');
888
+ process.exit(1);
889
+ }
890
+
891
+ return selectedBenchmarkIds;
892
+ }
893
+
894
+ async function resolveRunResolution(options, command, benchmarkIds) {
895
+ if (!shouldPromptForResolution(options, command)) {
896
+ return options.resolution;
897
+ }
898
+
899
+ const selectedResolution = await promptForResolution(options.resolution, benchmarkIds);
900
+ if (selectedResolution === null) {
901
+ prompts.cancel('Resolution selection cancelled');
902
+ process.exit(1);
903
+ }
904
+
905
+ return selectedResolution;
906
+ }
907
+
908
+ function formatResolutionDisplay(resolution, benchmarkIds) {
909
+ return resolution === null ? 'recorded source resolution' : resolution;
910
+ }
911
+
912
+ function printBenchmarkList() {
913
+ process.stdout.write('Available benchmarks:\n');
914
+ for (const benchmark of benchmarkCatalog) {
915
+ process.stdout.write(` ${benchmark.id} (${benchmark.label})\n`);
916
+ }
917
+ }
918
+
919
+ function formatPreparationMessage(command, selectedDisplay, resolutionDisplay) {
920
+ if (command === 'run') {
921
+ return `Preparing to run ${selectedDisplay}${resolutionDisplay}`;
922
+ }
923
+
924
+ return `Preparing to download ${selectedDisplay}`;
925
+ }
926
+
927
+ async function ensureBenchmarkAssets(selectionArgs, successMessage = 'Benchmark assets are ready') {
928
+ await runStreamingCommandWithSpinner(
929
+ 'Preparing benchmark downloads',
930
+ 'npm',
931
+ ['run', '-w=@luxonis/depthai-viewer-common', 'benchmark:download:raw', '--', ...selectionArgs],
932
+ {
933
+ env: {
934
+ ...process.env,
935
+ BENCHMARK_CURL_PROGRESS: '0',
936
+ },
937
+ createLineHandler: createDownloadStatusHandler,
938
+ successMessage,
939
+ },
940
+ );
941
+ }
942
+
943
+ async function main() {
944
+ const { command: parsedCommand, options } = parseArgs(process.argv.slice(2));
945
+ const returnToMainMenuAfterDownload = shouldPromptForAction(options, parsedCommand);
946
+ let command = parsedCommand;
947
+
948
+ if (options.help) {
949
+ printHelp();
950
+ return;
951
+ }
952
+
953
+ if (options.hasResolutionFlag) {
954
+ validateResolution(options.resolution);
955
+ }
956
+
957
+ for (;;) {
958
+ if (shouldPromptForAction(options, command)) {
959
+ command = await promptForAction();
960
+ if (command === null) {
961
+ prompts.cancel('Benchmark action cancelled');
962
+ process.exit(1);
963
+ }
964
+ }
965
+
966
+ if (command === null) {
967
+ command = 'run';
968
+ }
969
+
970
+ if (command === 'list') {
971
+ printBenchmarkList();
972
+ return;
973
+ }
974
+
975
+ if (!['run', 'download'].includes(command)) {
976
+ throw new Error(`Unknown command: ${command}`);
977
+ }
978
+
979
+ let benchmarkIds;
980
+ let resolvedResolution = options.resolution;
981
+ for (;;) {
982
+ benchmarkIds = await resolveBenchmarkIds(options);
983
+ if (benchmarkIds !== backValue) {
984
+ if (command !== 'run') {
985
+ break;
986
+ }
987
+
988
+ const nextResolution = await resolveRunResolution(options, command, benchmarkIds);
989
+ if (nextResolution === backValue) {
990
+ continue;
991
+ }
992
+
993
+ if (nextResolution === sourceResolutionValue) {
994
+ resolvedResolution = null;
995
+ break;
996
+ }
997
+
998
+ resolvedResolution = nextResolution;
999
+ if (resolvedResolution !== null) {
1000
+ validateResolution(resolvedResolution);
1001
+ }
1002
+ break;
1003
+ }
1004
+
1005
+ const nextCommand = await promptForAction();
1006
+ if (nextCommand === null) {
1007
+ prompts.cancel('Benchmark action cancelled');
1008
+ process.exit(1);
1009
+ }
1010
+
1011
+ command = nextCommand;
1012
+ }
1013
+
1014
+ const selectionArgs = toSelectionArgs(benchmarkIds);
1015
+ const resolutionArgs =
1016
+ command === 'run' && resolvedResolution !== null
1017
+ ? ['--resolution', resolvedResolution]
1018
+ : [];
1019
+ const selectedDisplay =
1020
+ benchmarkIds.length > 0 ? benchmarkIds.join(', ') : 'all benchmarks';
1021
+ const resolutionDisplay =
1022
+ command === 'run' ? ` • ${formatResolutionDisplay(resolvedResolution, benchmarkIds)}` : '';
1023
+
1024
+ prompts.intro(
1025
+ `DepthAI Benchmark CLI\n${formatPreparationMessage(command, selectedDisplay, resolutionDisplay)}`,
1026
+ );
1027
+
1028
+ if (command === 'run') {
1029
+ await ensureBenchmarkAssets(selectionArgs, 'Benchmark assets are ready');
1030
+ await runTaskWithSpinner(
1031
+ 'Building common package',
1032
+ 'npm',
1033
+ ['run', '-w=@luxonis/depthai-viewer-common', 'build'],
1034
+ {},
1035
+ );
1036
+ await runTaskWithSpinner(
1037
+ 'Building visualizer',
1038
+ 'npm',
1039
+ ['run', '-w=@luxonis/visualizer', 'build'],
1040
+ {},
1041
+ );
1042
+
1043
+ prompts.outro('Starting benchmark flow');
1044
+ await runCommand(
1045
+ rootRunCommand[0],
1046
+ [...rootRunCommand.slice(1), ...rootBenchmarkScript, ...selectionArgs, ...resolutionArgs],
1047
+ {
1048
+ env: {
1049
+ ...process.env,
1050
+ BENCHMARK_SKIP_DOWNLOAD: '1',
1051
+ },
1052
+ },
1053
+ );
1054
+ return;
1055
+ }
1056
+
1057
+ prompts.outro('Starting benchmark asset download');
1058
+ await ensureBenchmarkAssets(selectionArgs);
1059
+
1060
+ if (!returnToMainMenuAfterDownload) {
1061
+ return;
1062
+ }
1063
+
1064
+ command = null;
1065
+ }
1066
+ }
1067
+
1068
+ main().catch((error) => {
1069
+ prompts.cancel(error instanceof Error ? error.message : String(error));
1070
+ process.exit(1);
1071
+ });