@robin7331/papyrus-cli 0.1.10 → 0.1.11

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/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, join, relative, resolve } from "node:path";
6
6
  import { Command } from "commander";
7
7
  import { clearStoredApiKey, getConfigFilePath, getStoredApiKey, maskApiKey, setStoredApiKey } from "./config.js";
8
8
  import { assertModelAvailable, convertPdf, listAvailableModels, UnknownModelError } from "./openaiPdfToMarkdown.js";
9
- import { defaultOutputPath, formatDurationMs, isPdfPath, looksLikeFileOutput, parseConcurrency, parseFormat, resolveFolderOutputPath, truncate, validateOptionCombination } from "./cliHelpers.js";
9
+ import { defaultOutputPath, formatDurationMs, getSpinnerFrame, isPdfPath, looksLikeFileOutput, parseConcurrency, parseFormat, resolveFolderOutputPath, truncate, validateOptionCombination } from "./cliHelpers.js";
10
10
  const program = new Command();
11
11
  const configFilePath = getConfigFilePath();
12
12
  const OPENAI_API_KEYS_URL = "https://platform.openai.com/settings/organization/api-keys";
@@ -446,17 +446,20 @@ async function runWithConcurrency(items, concurrency, worker) {
446
446
  await Promise.all(workers);
447
447
  }
448
448
  class AsciiWorkerDashboard {
449
+ static spinnerIntervalMs = 80;
449
450
  lanes;
450
451
  total;
451
452
  workerCount;
452
453
  completed = 0;
453
454
  failed = 0;
454
455
  renderedLineCount = 0;
456
+ spinnerTimer;
455
457
  constructor(total, workerCount) {
456
458
  this.total = total;
457
459
  this.workerCount = workerCount;
458
460
  this.lanes = Array.from({ length: workerCount }, () => ({
459
- state: "idle"
461
+ state: "idle",
462
+ spinnerFrame: 0
460
463
  }));
461
464
  process.stdout.write("\x1b[?25l");
462
465
  this.render();
@@ -474,6 +477,8 @@ class AsciiWorkerDashboard {
474
477
  lane.state = "running";
475
478
  lane.file = file;
476
479
  lane.message = "processing...";
480
+ lane.spinnerFrame = 0;
481
+ this.syncSpinnerTimer();
477
482
  this.render();
478
483
  }
479
484
  setWorkerDone(workerId, file, message) {
@@ -484,6 +489,8 @@ class AsciiWorkerDashboard {
484
489
  lane.state = "done";
485
490
  lane.file = file;
486
491
  lane.message = message;
492
+ lane.spinnerFrame = 0;
493
+ this.syncSpinnerTimer();
487
494
  this.render();
488
495
  }
489
496
  setWorkerFailed(workerId, file, message) {
@@ -494,9 +501,12 @@ class AsciiWorkerDashboard {
494
501
  lane.state = "failed";
495
502
  lane.file = file;
496
503
  lane.message = message;
504
+ lane.spinnerFrame = 0;
505
+ this.syncSpinnerTimer();
497
506
  this.render();
498
507
  }
499
508
  stop() {
509
+ this.clearSpinnerTimer();
500
510
  this.render();
501
511
  process.stdout.write("\x1b[?25h");
502
512
  }
@@ -527,7 +537,7 @@ class AsciiWorkerDashboard {
527
537
  }
528
538
  renderIcon(lane) {
529
539
  if (lane.state === "running") {
530
- return ">>";
540
+ return `${getSpinnerFrame(lane.spinnerFrame)} `;
531
541
  }
532
542
  if (lane.state === "done") {
533
543
  return "OK";
@@ -537,6 +547,41 @@ class AsciiWorkerDashboard {
537
547
  }
538
548
  return "..";
539
549
  }
550
+ syncSpinnerTimer() {
551
+ if (this.lanes.some((lane) => lane.state === "running")) {
552
+ this.ensureSpinnerTimer();
553
+ return;
554
+ }
555
+ this.clearSpinnerTimer();
556
+ }
557
+ ensureSpinnerTimer() {
558
+ if (this.spinnerTimer) {
559
+ return;
560
+ }
561
+ this.spinnerTimer = setInterval(() => {
562
+ let hasRunningLane = false;
563
+ for (const lane of this.lanes) {
564
+ if (lane.state !== "running") {
565
+ continue;
566
+ }
567
+ lane.spinnerFrame += 1;
568
+ hasRunningLane = true;
569
+ }
570
+ if (!hasRunningLane) {
571
+ this.clearSpinnerTimer();
572
+ return;
573
+ }
574
+ this.render();
575
+ }, AsciiWorkerDashboard.spinnerIntervalMs);
576
+ this.spinnerTimer.unref?.();
577
+ }
578
+ clearSpinnerTimer() {
579
+ if (!this.spinnerTimer) {
580
+ return;
581
+ }
582
+ clearInterval(this.spinnerTimer);
583
+ this.spinnerTimer = undefined;
584
+ }
540
585
  }
541
586
  async function confirmFolderProcessing(totalFiles, concurrency, skipPrompt) {
542
587
  if (skipPrompt) {
@@ -17,4 +17,6 @@ export declare function resolveFolderOutputPath(inputPath: string, inputRoot: st
17
17
  export declare function isPdfPath(inputPath: string): boolean;
18
18
  export declare function looksLikeFileOutput(outputPath: string): boolean;
19
19
  export declare function truncate(value: string, maxLength: number): string;
20
+ export declare const ASCII_SPINNER_FRAMES: string[];
21
+ export declare function getSpinnerFrame(frameIndex: number): string;
20
22
  export declare function formatDurationMs(durationMs: number): string;
@@ -63,6 +63,12 @@ export function truncate(value, maxLength) {
63
63
  }
64
64
  return `${value.slice(0, maxLength - 3)}...`;
65
65
  }
66
+ export const ASCII_SPINNER_FRAMES = ["|", "/", "-", "\\"];
67
+ export function getSpinnerFrame(frameIndex) {
68
+ const normalizedIndex = ((Math.trunc(frameIndex) % ASCII_SPINNER_FRAMES.length) + ASCII_SPINNER_FRAMES.length)
69
+ % ASCII_SPINNER_FRAMES.length;
70
+ return ASCII_SPINNER_FRAMES[normalizedIndex] ?? ASCII_SPINNER_FRAMES[0];
71
+ }
66
72
  export function formatDurationMs(durationMs) {
67
73
  return `${(durationMs / 1000).toFixed(2)}s`;
68
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robin7331/papyrus-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "description": "Convert PDF to markdown or text with the OpenAI Agents SDK",
6
6
  "repository": {
@@ -37,7 +37,6 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@openai/agents": "^0.5.3",
40
- "@robin7331/papyrus-cli": "^0.1.4",
41
40
  "commander": "^14.0.0",
42
41
  "dotenv": "^17.3.1",
43
42
  "openai": "^6.7.0",
package/src/cli.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  import {
24
24
  defaultOutputPath,
25
25
  formatDurationMs,
26
+ getSpinnerFrame,
26
27
  isPdfPath,
27
28
  looksLikeFileOutput,
28
29
  parseConcurrency,
@@ -605,21 +606,25 @@ type WorkerLane = {
605
606
  state: "idle" | "running" | "done" | "failed";
606
607
  file?: string;
607
608
  message?: string;
609
+ spinnerFrame: number;
608
610
  };
609
611
 
610
612
  class AsciiWorkerDashboard {
613
+ private static readonly spinnerIntervalMs = 80;
611
614
  private readonly lanes: WorkerLane[];
612
615
  private readonly total: number;
613
616
  private readonly workerCount: number;
614
617
  private completed = 0;
615
618
  private failed = 0;
616
619
  private renderedLineCount = 0;
620
+ private spinnerTimer?: NodeJS.Timeout;
617
621
 
618
622
  constructor(total: number, workerCount: number) {
619
623
  this.total = total;
620
624
  this.workerCount = workerCount;
621
625
  this.lanes = Array.from({ length: workerCount }, () => ({
622
- state: "idle"
626
+ state: "idle",
627
+ spinnerFrame: 0
623
628
  }));
624
629
 
625
630
  process.stdout.write("\x1b[?25l");
@@ -641,6 +646,8 @@ class AsciiWorkerDashboard {
641
646
  lane.state = "running";
642
647
  lane.file = file;
643
648
  lane.message = "processing...";
649
+ lane.spinnerFrame = 0;
650
+ this.syncSpinnerTimer();
644
651
  this.render();
645
652
  }
646
653
 
@@ -653,6 +660,8 @@ class AsciiWorkerDashboard {
653
660
  lane.state = "done";
654
661
  lane.file = file;
655
662
  lane.message = message;
663
+ lane.spinnerFrame = 0;
664
+ this.syncSpinnerTimer();
656
665
  this.render();
657
666
  }
658
667
 
@@ -665,10 +674,13 @@ class AsciiWorkerDashboard {
665
674
  lane.state = "failed";
666
675
  lane.file = file;
667
676
  lane.message = message;
677
+ lane.spinnerFrame = 0;
678
+ this.syncSpinnerTimer();
668
679
  this.render();
669
680
  }
670
681
 
671
682
  stop(): void {
683
+ this.clearSpinnerTimer();
672
684
  this.render();
673
685
  process.stdout.write("\x1b[?25h");
674
686
  }
@@ -706,7 +718,7 @@ class AsciiWorkerDashboard {
706
718
 
707
719
  private renderIcon(lane: WorkerLane): string {
708
720
  if (lane.state === "running") {
709
- return ">>";
721
+ return `${getSpinnerFrame(lane.spinnerFrame)} `;
710
722
  }
711
723
 
712
724
  if (lane.state === "done") {
@@ -719,6 +731,50 @@ class AsciiWorkerDashboard {
719
731
 
720
732
  return "..";
721
733
  }
734
+
735
+ private syncSpinnerTimer(): void {
736
+ if (this.lanes.some((lane) => lane.state === "running")) {
737
+ this.ensureSpinnerTimer();
738
+ return;
739
+ }
740
+
741
+ this.clearSpinnerTimer();
742
+ }
743
+
744
+ private ensureSpinnerTimer(): void {
745
+ if (this.spinnerTimer) {
746
+ return;
747
+ }
748
+
749
+ this.spinnerTimer = setInterval(() => {
750
+ let hasRunningLane = false;
751
+ for (const lane of this.lanes) {
752
+ if (lane.state !== "running") {
753
+ continue;
754
+ }
755
+
756
+ lane.spinnerFrame += 1;
757
+ hasRunningLane = true;
758
+ }
759
+
760
+ if (!hasRunningLane) {
761
+ this.clearSpinnerTimer();
762
+ return;
763
+ }
764
+
765
+ this.render();
766
+ }, AsciiWorkerDashboard.spinnerIntervalMs);
767
+ this.spinnerTimer.unref?.();
768
+ }
769
+
770
+ private clearSpinnerTimer(): void {
771
+ if (!this.spinnerTimer) {
772
+ return;
773
+ }
774
+
775
+ clearInterval(this.spinnerTimer);
776
+ this.spinnerTimer = undefined;
777
+ }
722
778
  }
723
779
 
724
780
  async function confirmFolderProcessing(
package/src/cliHelpers.ts CHANGED
@@ -100,6 +100,14 @@ export function truncate(value: string, maxLength: number): string {
100
100
  return `${value.slice(0, maxLength - 3)}...`;
101
101
  }
102
102
 
103
+ export const ASCII_SPINNER_FRAMES = ["|", "/", "-", "\\"];
104
+
105
+ export function getSpinnerFrame(frameIndex: number): string {
106
+ const normalizedIndex = ((Math.trunc(frameIndex) % ASCII_SPINNER_FRAMES.length) + ASCII_SPINNER_FRAMES.length)
107
+ % ASCII_SPINNER_FRAMES.length;
108
+ return ASCII_SPINNER_FRAMES[normalizedIndex] ?? ASCII_SPINNER_FRAMES[0];
109
+ }
110
+
103
111
  export function formatDurationMs(durationMs: number): string {
104
112
  return `${(durationMs / 1000).toFixed(2)}s`;
105
113
  }
@@ -2,8 +2,10 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { InvalidArgumentError } from "commander";
4
4
  import {
5
+ ASCII_SPINNER_FRAMES,
5
6
  defaultOutputPath,
6
7
  formatDurationMs,
8
+ getSpinnerFrame,
7
9
  isPdfPath,
8
10
  looksLikeFileOutput,
9
11
  parseConcurrency,
@@ -130,6 +132,15 @@ test("truncate shortens long values and preserves short ones", () => {
130
132
  assert.equal(truncate("abcdefghij", 8), "abcde...");
131
133
  });
132
134
 
135
+ test("getSpinnerFrame cycles through the configured ASCII frames", () => {
136
+ assert.deepEqual(
137
+ ASCII_SPINNER_FRAMES.map((_, index) => getSpinnerFrame(index)),
138
+ ASCII_SPINNER_FRAMES
139
+ );
140
+ assert.equal(getSpinnerFrame(ASCII_SPINNER_FRAMES.length), ASCII_SPINNER_FRAMES[0]);
141
+ assert.equal(getSpinnerFrame(-1), ASCII_SPINNER_FRAMES.at(-1));
142
+ });
143
+
133
144
  test("formatDurationMs formats to seconds with two decimals", () => {
134
145
  assert.equal(formatDurationMs(0), "0.00s");
135
146
  assert.equal(formatDurationMs(1543), "1.54s");