@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 +48 -3
- package/dist/cliHelpers.d.ts +2 -0
- package/dist/cliHelpers.js +6 -0
- package/package.json +1 -2
- package/src/cli.ts +58 -2
- package/src/cliHelpers.ts +8 -0
- package/test/cliHelpers.test.ts +11 -0
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) {
|
package/dist/cliHelpers.d.ts
CHANGED
|
@@ -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;
|
package/dist/cliHelpers.js
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/test/cliHelpers.test.ts
CHANGED
|
@@ -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");
|