readr-cli 1.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.
@@ -0,0 +1,12 @@
1
+ import { Book, Session } from './types.js';
2
+ export declare function progressBar(current: number, total: number, width?: number): string;
3
+ export declare function formatDuration(ms: number): string;
4
+ export declare function getBookStats(book: Book, sessions: Session[]): {
5
+ totalMs: number;
6
+ totalPagesRead: number;
7
+ pagesPerHour: number;
8
+ etaMs: number | null;
9
+ sessionCount: number;
10
+ };
11
+ export declare function printBookCard(book: Book, sessions: Session[], activeSessionId?: string): void;
12
+ export declare function printSessionSummary(session: Session, book: Book): void;
@@ -0,0 +1,74 @@
1
+ import chalk from 'chalk';
2
+ export function progressBar(current, total, width = 30) {
3
+ const pct = Math.min(current / total, 1);
4
+ const filled = Math.round(pct * width);
5
+ const empty = width - filled;
6
+ const bar = chalk.green('โ–ˆ'.repeat(filled)) + chalk.gray('โ–‘'.repeat(empty));
7
+ return `[${bar}] ${chalk.bold((pct * 100).toFixed(1))}%`;
8
+ }
9
+ export function formatDuration(ms) {
10
+ const totalSec = Math.floor(ms / 1000);
11
+ const h = Math.floor(totalSec / 3600);
12
+ const m = Math.floor((totalSec % 3600) / 60);
13
+ const s = totalSec % 60;
14
+ if (h > 0)
15
+ return `${h}h ${m}m`;
16
+ if (m > 0)
17
+ return `${m}m ${s}s`;
18
+ return `${s}s`;
19
+ }
20
+ export function getBookStats(book, sessions) {
21
+ const bookSessions = sessions.filter((s) => s.bookId === book.id && s.status === 'completed');
22
+ const totalMs = bookSessions.reduce((acc, s) => {
23
+ if (!s.endTime)
24
+ return acc;
25
+ const raw = new Date(s.endTime).getTime() - new Date(s.startTime).getTime();
26
+ return acc + raw - s.totalPausedMs;
27
+ }, 0);
28
+ const totalPagesRead = bookSessions.reduce((acc, s) => {
29
+ return acc + ((s.endPage ?? s.startPage) - s.startPage);
30
+ }, 0);
31
+ const pagesPerHour = totalMs > 0 ? (totalPagesRead / totalMs) * 3600000 : 0;
32
+ const pagesLeft = book.totalPages - book.currentPage;
33
+ const etaMs = pagesPerHour > 0 ? (pagesLeft / pagesPerHour) * 3600000 : null;
34
+ return { totalMs, totalPagesRead, pagesPerHour, etaMs, sessionCount: bookSessions.length };
35
+ }
36
+ export function printBookCard(book, sessions, activeSessionId) {
37
+ const stats = getBookStats(book, sessions);
38
+ const isActive = sessions.find((s) => s.bookId === book.id && (s.status === 'active' || s.status === 'paused'));
39
+ console.log('');
40
+ console.log(chalk.bold.cyan(`๐Ÿ“– ${book.title}`) + chalk.gray(` by ${book.author}`));
41
+ console.log(` ${progressBar(book.currentPage, book.totalPages)}`);
42
+ console.log(` ${chalk.yellow('Page:')} ${chalk.bold(book.currentPage)} / ${book.totalPages} ` +
43
+ `${chalk.yellow('Sessions:')} ${stats.sessionCount} ` +
44
+ `${chalk.yellow('Time:')} ${formatDuration(stats.totalMs)}`);
45
+ if (stats.pagesPerHour > 0) {
46
+ console.log(` ${chalk.yellow('Speed:')} ${stats.pagesPerHour.toFixed(1)} pages/hr ` +
47
+ (stats.etaMs !== null
48
+ ? `${chalk.yellow('ETA:')} ${chalk.bold.green(formatDuration(stats.etaMs))} left`
49
+ : ''));
50
+ }
51
+ else {
52
+ console.log(` ${chalk.gray('No speed data yet โ€” start a session!')}`);
53
+ }
54
+ if (isActive) {
55
+ const status = isActive.status === 'paused' ? chalk.yellow('โธ PAUSED') : chalk.green('โ–ถ READING');
56
+ console.log(` ${status} ${chalk.gray(`from page ${isActive.startPage}`)}`);
57
+ }
58
+ console.log('');
59
+ }
60
+ export function printSessionSummary(session, book) {
61
+ const pagesRead = (session.endPage ?? session.startPage) - session.startPage;
62
+ const rawMs = new Date(session.endTime).getTime() - new Date(session.startTime).getTime();
63
+ const activeMs = rawMs - session.totalPausedMs;
64
+ const speed = activeMs > 0 ? (pagesRead / activeMs) * 3600000 : 0;
65
+ console.log('');
66
+ console.log(chalk.bold.green('โœ… Session complete!'));
67
+ console.log(chalk.gray('โ”€'.repeat(40)));
68
+ console.log(` ${chalk.yellow('Book:')} ${book.title}`);
69
+ console.log(` ${chalk.yellow('Pages:')} ${session.startPage} โ†’ ${session.endPage} ${chalk.bold('(+' + pagesRead + ')')}`);
70
+ console.log(` ${chalk.yellow('Duration:')} ${formatDuration(activeMs)}`);
71
+ console.log(` ${chalk.yellow('Speed:')} ${speed.toFixed(1)} pages/hr`);
72
+ console.log(chalk.gray('โ”€'.repeat(40)));
73
+ console.log('');
74
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ import readline from 'readline';
3
+ import chalk from 'chalk';
4
+ import { loadStore, saveStore, generateId } from './store.js';
5
+ import { printBookCard, printSessionSummary, progressBar, formatDuration, getBookStats } from './display.js';
6
+ const args = process.argv.slice(2);
7
+ const command = args[0];
8
+ function prompt(question) {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
+ return new Promise((resolve) => {
11
+ rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); });
12
+ });
13
+ }
14
+ function header() {
15
+ console.log('');
16
+ console.log(chalk.bold.magenta(' ๐Ÿ“š Reading Tracker'));
17
+ console.log(chalk.gray(' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
18
+ }
19
+ async function addBook() {
20
+ header();
21
+ const title = await prompt(chalk.cyan(' Book title: '));
22
+ const author = await prompt(chalk.cyan(' Author: '));
23
+ const totalPagesStr = await prompt(chalk.cyan(' Total pages: '));
24
+ const currentPageStr = await prompt(chalk.cyan(' Current page (0 if new): '));
25
+ const totalPages = parseInt(totalPagesStr);
26
+ const currentPage = parseInt(currentPageStr) || 0;
27
+ if (!title || isNaN(totalPages)) {
28
+ console.log(chalk.red(' โœ— Invalid input.'));
29
+ process.exit(1);
30
+ }
31
+ const store = loadStore();
32
+ const book = {
33
+ id: generateId(),
34
+ title,
35
+ author,
36
+ totalPages,
37
+ currentPage,
38
+ addedAt: new Date().toISOString(),
39
+ };
40
+ store.books.push(book);
41
+ saveStore(store);
42
+ console.log(chalk.green(`\n โœ“ Added "${title}"!`));
43
+ printBookCard(book, []);
44
+ }
45
+ async function startSession() {
46
+ const store = loadStore();
47
+ const existing = store.sessions.find((s) => s.status === 'active' || s.status === 'paused');
48
+ if (existing) {
49
+ const book = store.books.find((b) => b.id === existing.bookId);
50
+ console.log(chalk.yellow(`\n โš  You already have a session for "${book.title}" (${existing.status})`));
51
+ console.log(chalk.gray(' Use: read pause | read stop\n'));
52
+ process.exit(1);
53
+ }
54
+ if (store.books.length === 0) {
55
+ console.log(chalk.red('\n No books found. Add one with: read add\n'));
56
+ process.exit(1);
57
+ }
58
+ header();
59
+ console.log(chalk.bold(' Your books:\n'));
60
+ store.books.forEach((b, i) => {
61
+ console.log(` ${chalk.bold.cyan(i + 1 + '.')} ${b.title} ${chalk.gray(`โ€” page ${b.currentPage}/${b.totalPages}`)}`);
62
+ });
63
+ const choiceStr = await prompt(chalk.cyan('\n Pick a book (number): '));
64
+ const choice = parseInt(choiceStr) - 1;
65
+ const book = store.books[choice];
66
+ if (!book) {
67
+ console.log(chalk.red(' โœ— Invalid choice.'));
68
+ process.exit(1);
69
+ }
70
+ const startPageStr = await prompt(chalk.cyan(` Starting from page (enter for ${book.currentPage}): `));
71
+ const startPage = parseInt(startPageStr) || book.currentPage;
72
+ const session = {
73
+ id: generateId(),
74
+ bookId: book.id,
75
+ startPage,
76
+ startTime: new Date().toISOString(),
77
+ totalPausedMs: 0,
78
+ status: 'active',
79
+ };
80
+ store.sessions.push(session);
81
+ store.activeSessionId = session.id;
82
+ saveStore(store);
83
+ console.log('');
84
+ console.log(chalk.green(` โ–ถ Session started for "${book.title}"`));
85
+ console.log(chalk.gray(` From page ${startPage} โ€” happy reading! ๐Ÿ“–`));
86
+ console.log('');
87
+ }
88
+ function pauseSession() {
89
+ const store = loadStore();
90
+ const session = store.sessions.find((s) => s.status === 'active' || s.status === 'paused');
91
+ if (!session) {
92
+ console.log(chalk.red('\n No active session. Start one with: read start\n'));
93
+ process.exit(1);
94
+ }
95
+ const book = store.books.find((b) => b.id === session.bookId);
96
+ if (session.status === 'paused') {
97
+ const pausedMs = new Date().getTime() - new Date(session.pausedAt).getTime();
98
+ session.totalPausedMs += pausedMs;
99
+ session.pausedAt = undefined;
100
+ session.status = 'active';
101
+ saveStore(store);
102
+ console.log(chalk.green(`\n โ–ถ Resumed "${book.title}"\n`));
103
+ }
104
+ else {
105
+ session.pausedAt = new Date().toISOString();
106
+ session.status = 'paused';
107
+ saveStore(store);
108
+ const elapsed = new Date().getTime() - new Date(session.startTime).getTime() - session.totalPausedMs;
109
+ console.log(chalk.yellow(`\n โธ Paused "${book.title}" โ€” ${formatDuration(elapsed)} so far\n`));
110
+ }
111
+ }
112
+ async function stopSession() {
113
+ const store = loadStore();
114
+ const session = store.sessions.find((s) => s.status === 'active' || s.status === 'paused');
115
+ if (!session) {
116
+ console.log(chalk.red('\n No active session. Start one with: read start\n'));
117
+ process.exit(1);
118
+ }
119
+ const book = store.books.find((b) => b.id === session.bookId);
120
+ const endPageStr = await prompt(chalk.cyan(`\n Ending on page (enter for current ${book.currentPage}): `));
121
+ const endPage = parseInt(endPageStr) || book.currentPage;
122
+ if (session.status === 'paused' && session.pausedAt) {
123
+ session.totalPausedMs += new Date().getTime() - new Date(session.pausedAt).getTime();
124
+ }
125
+ session.endPage = endPage;
126
+ session.endTime = new Date().toISOString();
127
+ session.status = 'completed';
128
+ delete store.activeSessionId;
129
+ book.currentPage = endPage;
130
+ saveStore(store);
131
+ printSessionSummary(session, book);
132
+ printBookCard(book, store.sessions);
133
+ }
134
+ function listBooks() {
135
+ const store = loadStore();
136
+ if (store.books.length === 0) {
137
+ console.log(chalk.gray('\n No books yet. Add one with: read add\n'));
138
+ return;
139
+ }
140
+ header();
141
+ for (const book of store.books) {
142
+ printBookCard(book, store.sessions, store.activeSessionId);
143
+ }
144
+ }
145
+ function showStats() {
146
+ const store = loadStore();
147
+ if (store.books.length === 0) {
148
+ console.log(chalk.gray('\n No books yet. Add one with: read add\n'));
149
+ return;
150
+ }
151
+ header();
152
+ console.log(chalk.bold(' ๐Ÿ“Š Overall Stats\n'));
153
+ const completedSessions = store.sessions.filter((s) => s.status === 'completed');
154
+ const totalMs = completedSessions.reduce((acc, s) => {
155
+ if (!s.endTime)
156
+ return acc;
157
+ return acc + (new Date(s.endTime).getTime() - new Date(s.startTime).getTime() - s.totalPausedMs);
158
+ }, 0);
159
+ const totalPages = completedSessions.reduce((acc, s) => {
160
+ return acc + ((s.endPage ?? s.startPage) - s.startPage);
161
+ }, 0);
162
+ console.log(` ${chalk.yellow('Total reading time:')} ${chalk.bold(formatDuration(totalMs))}`);
163
+ console.log(` ${chalk.yellow('Total pages read:')} ${chalk.bold(totalPages)}`);
164
+ console.log(` ${chalk.yellow('Total sessions:')} ${chalk.bold(completedSessions.length)}`);
165
+ console.log('');
166
+ for (const book of store.books) {
167
+ const stats = getBookStats(book, store.sessions);
168
+ console.log(` ${chalk.bold.cyan(book.title)}`);
169
+ console.log(` ${progressBar(book.currentPage, book.totalPages, 25)}`);
170
+ if (stats.pagesPerHour > 0) {
171
+ console.log(` ${chalk.gray(`${book.currentPage}/${book.totalPages} pages ยท ${stats.pagesPerHour.toFixed(1)} pg/hr ยท ETA: ${stats.etaMs !== null ? formatDuration(stats.etaMs) : '?'}`)}`);
172
+ }
173
+ console.log('');
174
+ }
175
+ }
176
+ function showHelp() {
177
+ header();
178
+ console.log(chalk.bold(' Commands:\n'));
179
+ const cmds = [
180
+ ['read add', 'Add a new book'],
181
+ ['read start', 'Start a reading session'],
182
+ ['read pause', 'Pause or resume current session'],
183
+ ['read stop', 'End session & log pages read'],
184
+ ['read list', 'List all books with progress'],
185
+ ['read stats', 'Overall reading statistics'],
186
+ ['read help', 'Show this help'],
187
+ ];
188
+ for (const [cmd, desc] of cmds) {
189
+ console.log(` ${chalk.bold.green(cmd.padEnd(14))} ${chalk.gray(desc)}`);
190
+ }
191
+ console.log('');
192
+ }
193
+ (async () => {
194
+ switch (command) {
195
+ case 'add':
196
+ await addBook();
197
+ break;
198
+ case 'start':
199
+ await startSession();
200
+ break;
201
+ case 'pause':
202
+ pauseSession();
203
+ break;
204
+ case 'stop':
205
+ await stopSession();
206
+ break;
207
+ case 'list':
208
+ listBooks();
209
+ break;
210
+ case 'stats':
211
+ showStats();
212
+ break;
213
+ case 'help':
214
+ default:
215
+ showHelp();
216
+ break;
217
+ }
218
+ })();
@@ -0,0 +1,4 @@
1
+ import { Store } from './types.js';
2
+ export declare function loadStore(): Store;
3
+ export declare function saveStore(store: Store): void;
4
+ export declare function generateId(): string;
package/dist/store.js ADDED
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const STORE_PATH = path.join(os.homedir(), '.reading-cli', 'data.json');
5
+ const DEFAULT_STORE = {
6
+ books: [],
7
+ sessions: [],
8
+ };
9
+ export function loadStore() {
10
+ try {
11
+ if (!fs.existsSync(STORE_PATH)) {
12
+ fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
13
+ fs.writeFileSync(STORE_PATH, JSON.stringify(DEFAULT_STORE, null, 2));
14
+ return DEFAULT_STORE;
15
+ }
16
+ return JSON.parse(fs.readFileSync(STORE_PATH, 'utf-8'));
17
+ }
18
+ catch {
19
+ return { ...DEFAULT_STORE };
20
+ }
21
+ }
22
+ export function saveStore(store) {
23
+ fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
24
+ fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
25
+ }
26
+ export function generateId() {
27
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
28
+ }
@@ -0,0 +1,25 @@
1
+ export interface Book {
2
+ id: string;
3
+ title: string;
4
+ author: string;
5
+ totalPages: number;
6
+ currentPage: number;
7
+ addedAt: string;
8
+ finishedAt?: string;
9
+ }
10
+ export interface Session {
11
+ id: string;
12
+ bookId: string;
13
+ startPage: number;
14
+ endPage?: number;
15
+ startTime: string;
16
+ endTime?: string;
17
+ pausedAt?: string;
18
+ totalPausedMs: number;
19
+ status: 'active' | 'paused' | 'completed';
20
+ }
21
+ export interface Store {
22
+ books: Book[];
23
+ sessions: Session[];
24
+ activeSessionId?: string;
25
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "readr-cli",
3
+ "version": "1.0.0",
4
+ "description": "Beautiful CLI to track reading sessions, speed, and book completion ETA",
5
+ "type": "module",
6
+ "bin": {
7
+ "reading": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx src/index.ts",
11
+ "build": "tsc",
12
+ "prepare": "npm run build"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "keywords": ["reading", "tracker", "cli", "books", "progress"],
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "chalk": "^5.3.0",
24
+ "cli-table3": "^0.6.5"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.13.4",
28
+ "tsx": "^4.19.3",
29
+ "typescript": "^5.7.3"
30
+ }
31
+ }