pom-tool 0.1.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.
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +355 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ming
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
## pom-tool
|
|
2
|
+
|
|
3
|
+
A small **Node.js CLI** (TypeScript + pnpm) for running Pomodoro timers in your terminal, with optional **Bark** notifications synced to your phone.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Run a Pomodoro for N minutes**
|
|
8
|
+
- `pom 25` starts a 25-minute Pomodoro.
|
|
9
|
+
- **View stats**
|
|
10
|
+
- `pom status` shows average Pomodoro minutes for:
|
|
11
|
+
- today
|
|
12
|
+
- the last 7 days
|
|
13
|
+
- the last 30 days
|
|
14
|
+
- **Bark notifications**
|
|
15
|
+
- `pom bark` configures a Bark URL so the CLI can push notifications to the Bark app on your iPhone.
|
|
16
|
+
- `pom bark --url <BARK_URL>` supports non-interactive setup.
|
|
17
|
+
- **Help**
|
|
18
|
+
- `pom --help` shows the help message.
|
|
19
|
+
- `pom help` shows the help message.
|
|
20
|
+
- `pom -h` shows the help message.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Node.js (recommended: an active LTS version)
|
|
25
|
+
|
|
26
|
+
## Install (from npm)
|
|
27
|
+
|
|
28
|
+
Install globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm i -g pom-tool
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or use without installing (recommended for quick usage):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx pom-tool 25
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
After installing, the command is:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pom
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### Start a Pomodoro
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pom 25
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Check status / averages
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pom status
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Configure Bark
|
|
61
|
+
|
|
62
|
+
1. Get your Bark push URL from the Bark app (it usually looks like `https://api.day.app/<your_key>`).
|
|
63
|
+
2. Set it via the CLI:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pom bark
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
After it’s configured, the CLI will send important notifications (for example: timer started/finished) to Bark.
|
|
70
|
+
|
|
71
|
+
For non-interactive setup:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pom bark --url https://api.day.app/<your_key>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
This repo uses **pnpm + TypeScript**.
|
|
80
|
+
|
|
81
|
+
### Setup
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone <your-repo-url>
|
|
85
|
+
cd pom-tool
|
|
86
|
+
pnpm install
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Run locally
|
|
90
|
+
|
|
91
|
+
Common workflows for Node CLIs:
|
|
92
|
+
|
|
93
|
+
- Build then run the compiled CLI:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pnpm build
|
|
97
|
+
node dist/index.js 25
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- Or run directly with a TS runner (if configured in this repo later):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpm dev -- 25
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Link for local testing (global `pom`)
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pnpm build
|
|
110
|
+
pnpm link --global
|
|
111
|
+
pom 25
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
To unlink:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm unlink --global pom-tool
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Versioning
|
|
121
|
+
|
|
122
|
+
This package follows **Semantic Versioning** (SemVer): `MAJOR.MINOR.PATCH`.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
See `LICENSE`.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process, { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
import readline from "node:readline/promises";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
const ANSI = {
|
|
10
|
+
reset: "\u001b[0m",
|
|
11
|
+
bold: "\u001b[1m",
|
|
12
|
+
dim: "\u001b[2m",
|
|
13
|
+
red: "\u001b[31m",
|
|
14
|
+
green: "\u001b[32m",
|
|
15
|
+
yellow: "\u001b[33m",
|
|
16
|
+
blue: "\u001b[34m",
|
|
17
|
+
cyan: "\u001b[36m",
|
|
18
|
+
gray: "\u001b[90m",
|
|
19
|
+
};
|
|
20
|
+
const DATA_DIR = process.env.POM_TOOL_DATA_DIR || path.join(os.homedir(), ".pom-tool");
|
|
21
|
+
const CONFIG_PATH = path.join(DATA_DIR, "config.json");
|
|
22
|
+
const SESSIONS_PATH = path.join(DATA_DIR, "sessions.json");
|
|
23
|
+
const COLOR_ENABLED = process.stdout.isTTY &&
|
|
24
|
+
!("NO_COLOR" in process.env) &&
|
|
25
|
+
process.env.TERM !== "dumb";
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
27
|
+
async function main() {
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const command = args[0];
|
|
30
|
+
if (!command) {
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
36
|
+
printHelp();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (command === "status") {
|
|
40
|
+
await showStatus();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (command === "bark") {
|
|
44
|
+
await configureBark(args.slice(1));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const minutes = Number(command);
|
|
48
|
+
if (!Number.isInteger(minutes) || minutes <= 0) {
|
|
49
|
+
console.error(`Invalid argument: "${command}"`);
|
|
50
|
+
printHelp();
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await runPomodoro(minutes);
|
|
55
|
+
}
|
|
56
|
+
function printHelp() {
|
|
57
|
+
console.log(renderPanel("pom-tool", [
|
|
58
|
+
`${label("Usage")} ${style("pom <minutes>", "cyan")} Start a Pomodoro timer`,
|
|
59
|
+
` ${style("pom status", "cyan")} Show Pomodoro averages`,
|
|
60
|
+
` ${style("pom bark [--url URL]", "cyan")} Configure Bark notifications`,
|
|
61
|
+
]));
|
|
62
|
+
}
|
|
63
|
+
async function ensureDataDir() {
|
|
64
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
async function readJsonFile(filePath, fallback) {
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(filePath, "utf8");
|
|
69
|
+
return JSON.parse(content);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code === "ENOENT") {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function writeJsonFile(filePath, value) {
|
|
79
|
+
await ensureDataDir();
|
|
80
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
81
|
+
}
|
|
82
|
+
async function readStore() {
|
|
83
|
+
const [config, sessions] = await Promise.all([
|
|
84
|
+
readJsonFile(CONFIG_PATH, {}),
|
|
85
|
+
readJsonFile(SESSIONS_PATH, []),
|
|
86
|
+
]);
|
|
87
|
+
return { config, sessions };
|
|
88
|
+
}
|
|
89
|
+
async function saveConfig(config) {
|
|
90
|
+
await writeJsonFile(CONFIG_PATH, config);
|
|
91
|
+
}
|
|
92
|
+
async function appendSession(record) {
|
|
93
|
+
const sessions = await readJsonFile(SESSIONS_PATH, []);
|
|
94
|
+
sessions.push(record);
|
|
95
|
+
await writeJsonFile(SESSIONS_PATH, sessions);
|
|
96
|
+
}
|
|
97
|
+
async function configureBark(args) {
|
|
98
|
+
const inlineUrl = readFlagValue(args, "--url");
|
|
99
|
+
const currentConfig = await readJsonFile(CONFIG_PATH, {});
|
|
100
|
+
const barkUrl = inlineUrl ?? (await promptForBarkUrl(currentConfig.barkUrl));
|
|
101
|
+
if (!barkUrl) {
|
|
102
|
+
console.log(style("Bark setup cancelled.", "yellow"));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
validateBarkUrl(barkUrl);
|
|
106
|
+
await saveConfig({ ...currentConfig, barkUrl });
|
|
107
|
+
console.log(renderPanel("Bark", [
|
|
108
|
+
`${label("Status")} ${style("saved", "green")}`,
|
|
109
|
+
`${label("URL")} ${maskBarkUrl(barkUrl)}`,
|
|
110
|
+
]));
|
|
111
|
+
}
|
|
112
|
+
function readFlagValue(args, flag) {
|
|
113
|
+
const index = args.indexOf(flag);
|
|
114
|
+
if (index === -1) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const value = args[index + 1];
|
|
118
|
+
if (!value || value.startsWith("--")) {
|
|
119
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
async function promptForBarkUrl(existingUrl) {
|
|
124
|
+
const rl = readline.createInterface({ input, output });
|
|
125
|
+
try {
|
|
126
|
+
const hint = existingUrl ? ` [current: ${existingUrl}]` : "";
|
|
127
|
+
const answer = await rl.question(`Enter Bark URL${hint}: `);
|
|
128
|
+
const nextUrl = answer.trim();
|
|
129
|
+
return nextUrl || existingUrl;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
rl.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function validateBarkUrl(barkUrl) {
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = new URL(barkUrl);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw new Error("Bark URL is not a valid URL.");
|
|
142
|
+
}
|
|
143
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
144
|
+
throw new Error("Bark URL must use http or https.");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function runPomodoro(minutes) {
|
|
148
|
+
const { config } = await readStore();
|
|
149
|
+
const totalSeconds = minutes * 60;
|
|
150
|
+
const startedAt = new Date();
|
|
151
|
+
console.log(renderPanel("🍅 Pomodoro", [
|
|
152
|
+
`${label("Duration")} ${style(formatMinutes(minutes), "cyan")}`,
|
|
153
|
+
`${label("Started")} ${formatClockTime(startedAt)}`,
|
|
154
|
+
`${label("Bark")} ${config.barkUrl ? style("enabled", "green") : style("disabled", "gray")}`,
|
|
155
|
+
]));
|
|
156
|
+
const interrupted = await startCountdown(totalSeconds);
|
|
157
|
+
if (interrupted) {
|
|
158
|
+
console.log(`\n${style("Pomodoro cancelled.", "yellow")}`);
|
|
159
|
+
process.exitCode = 130;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const finishedAt = new Date();
|
|
163
|
+
await appendSession({
|
|
164
|
+
completedAt: toIsoSeconds(finishedAt),
|
|
165
|
+
minutes,
|
|
166
|
+
});
|
|
167
|
+
await notifyPomodoroFinished(minutes, finishedAt);
|
|
168
|
+
process.stdout.write("\n");
|
|
169
|
+
console.log(renderPanel("🍅 Completed", [
|
|
170
|
+
`${label("Logged")} ${style(formatMinutes(minutes), "green")}`,
|
|
171
|
+
`${label("Finished")} ${formatClockTime(finishedAt)}`,
|
|
172
|
+
`${label("Saved")} ${style("session recorded", "green")}`,
|
|
173
|
+
]));
|
|
174
|
+
await sendBarkNotification(config.barkUrl, "Pomodoro finished", `${minutes} minute${minutes === 1 ? "" : "s"} completed at ${formatClockTime(finishedAt)}`);
|
|
175
|
+
}
|
|
176
|
+
async function startCountdown(totalSeconds) {
|
|
177
|
+
let interrupted = false;
|
|
178
|
+
const onSigint = () => {
|
|
179
|
+
interrupted = true;
|
|
180
|
+
};
|
|
181
|
+
process.once("SIGINT", onSigint);
|
|
182
|
+
try {
|
|
183
|
+
for (let remaining = totalSeconds; remaining >= 0; remaining -= 1) {
|
|
184
|
+
renderRemaining(remaining);
|
|
185
|
+
if (remaining === 0) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
await sleep(1000);
|
|
189
|
+
if (interrupted) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
process.off("SIGINT", onSigint);
|
|
196
|
+
}
|
|
197
|
+
return interrupted;
|
|
198
|
+
}
|
|
199
|
+
function renderRemaining(totalSeconds) {
|
|
200
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
201
|
+
const seconds = totalSeconds % 60;
|
|
202
|
+
const timestamp = `${pad(minutes)}:${pad(seconds)}`;
|
|
203
|
+
const reset = COLOR_ENABLED ? ANSI.reset : "";
|
|
204
|
+
process.stdout.write(`\r⏳ ${style("Timer", "blue")} ${style(timestamp, totalSeconds <= 10 ? "yellow" : "cyan")}${reset}`);
|
|
205
|
+
}
|
|
206
|
+
function pad(value) {
|
|
207
|
+
return value.toString().padStart(2, "0");
|
|
208
|
+
}
|
|
209
|
+
function sleep(ms) {
|
|
210
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
211
|
+
}
|
|
212
|
+
async function sendBarkNotification(barkUrl, title, body) {
|
|
213
|
+
if (!barkUrl) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const url = new URL(barkUrl);
|
|
218
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/${encodeURIComponent(title)}/${encodeURIComponent(body)}`;
|
|
219
|
+
const response = await fetch(url, { method: "GET" });
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
console.warn(`Bark notification failed with status ${response.status}.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.warn(`Bark notification failed: ${error.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function notifyPomodoroFinished(minutes, finishedAt) {
|
|
229
|
+
playTerminalBell();
|
|
230
|
+
await maybeSendSystemNotification(minutes, finishedAt);
|
|
231
|
+
}
|
|
232
|
+
function playTerminalBell() {
|
|
233
|
+
process.stdout.write("\u0007");
|
|
234
|
+
}
|
|
235
|
+
async function maybeSendSystemNotification(minutes, finishedAt) {
|
|
236
|
+
if (process.platform !== "darwin") {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const message = `Pomodoro finished. ${formatMinutes(minutes)} completed at ${formatClockTime(finishedAt)}.`;
|
|
240
|
+
try {
|
|
241
|
+
await execFileAsync("/usr/bin/say", [message]);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Best-effort only. Terminal bell already fired.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function showStatus() {
|
|
248
|
+
const { config, sessions } = await readStore();
|
|
249
|
+
const today = new Date();
|
|
250
|
+
const todayTotal = sumMinutesSince(sessions, startOfDay(today));
|
|
251
|
+
const last7Total = sumMinutesSince(sessions, daysAgo(today, 6));
|
|
252
|
+
const last30Total = sumMinutesSince(sessions, daysAgo(today, 29));
|
|
253
|
+
const lastSession = getLastSession(sessions);
|
|
254
|
+
console.log(renderPanel("Pomodoro Status", [
|
|
255
|
+
`${label("Today")} ${style(formatMinutesDecimal(todayTotal), "green")}`,
|
|
256
|
+
`${label("7d avg")} ${style(`${formatMinutesDecimal(last7Total / 7)}/day`, "cyan")}`,
|
|
257
|
+
`${label("30d avg")} ${style(`${formatMinutesDecimal(last30Total / 30)}/day`, "cyan")}`,
|
|
258
|
+
`${label("Sessions")} ${style(String(sessions.length), "blue")}`,
|
|
259
|
+
`${label("Bark")} ${config.barkUrl ? style("configured", "green") : style("not configured", "gray")}`,
|
|
260
|
+
`${label("Last done")} ${lastSession ? formatSessionTime(lastSession.completedAt) : style("none yet", "gray")}`,
|
|
261
|
+
]));
|
|
262
|
+
}
|
|
263
|
+
function sumMinutesSince(sessions, since) {
|
|
264
|
+
return sessions.reduce((total, session) => {
|
|
265
|
+
const completedAt = new Date(session.completedAt);
|
|
266
|
+
return completedAt >= since ? total + session.minutes : total;
|
|
267
|
+
}, 0);
|
|
268
|
+
}
|
|
269
|
+
function startOfDay(date) {
|
|
270
|
+
const next = new Date(date);
|
|
271
|
+
next.setHours(0, 0, 0, 0);
|
|
272
|
+
return next;
|
|
273
|
+
}
|
|
274
|
+
function daysAgo(date, days) {
|
|
275
|
+
const next = startOfDay(date);
|
|
276
|
+
next.setDate(next.getDate() - days);
|
|
277
|
+
return next;
|
|
278
|
+
}
|
|
279
|
+
function getLastSession(sessions) {
|
|
280
|
+
return sessions.reduce((latest, session) => {
|
|
281
|
+
if (!latest) {
|
|
282
|
+
return session;
|
|
283
|
+
}
|
|
284
|
+
return new Date(session.completedAt) > new Date(latest.completedAt)
|
|
285
|
+
? session
|
|
286
|
+
: latest;
|
|
287
|
+
}, undefined);
|
|
288
|
+
}
|
|
289
|
+
function formatSessionTime(value) {
|
|
290
|
+
return new Date(value).toLocaleString([], {
|
|
291
|
+
hour12: false,
|
|
292
|
+
year: "numeric",
|
|
293
|
+
month: "2-digit",
|
|
294
|
+
day: "2-digit",
|
|
295
|
+
hour: "2-digit",
|
|
296
|
+
minute: "2-digit",
|
|
297
|
+
second: "2-digit",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function formatMinutes(minutes) {
|
|
301
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
302
|
+
}
|
|
303
|
+
function formatMinutesDecimal(minutes) {
|
|
304
|
+
return `${minutes.toFixed(1)} min`;
|
|
305
|
+
}
|
|
306
|
+
function formatClockTime(date) {
|
|
307
|
+
return date.toLocaleTimeString([], {
|
|
308
|
+
hour12: false,
|
|
309
|
+
hour: "2-digit",
|
|
310
|
+
minute: "2-digit",
|
|
311
|
+
second: "2-digit",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function toIsoSeconds(date) {
|
|
315
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
316
|
+
}
|
|
317
|
+
function label(text) {
|
|
318
|
+
return style(text.padEnd(9, " "), "gray");
|
|
319
|
+
}
|
|
320
|
+
function renderPanel(title, lines) {
|
|
321
|
+
const width = Math.max(title.length, ...lines.map(stripAnsi).map((line) => line.length));
|
|
322
|
+
const top = `+${"-".repeat(width + 2)}+`;
|
|
323
|
+
const header = `| ${style(title.padEnd(width, " "), "bold")} |`;
|
|
324
|
+
const body = lines.map((line) => `| ${padAnsi(line, width)} |`);
|
|
325
|
+
return [top, header, top, ...body, top].join("\n");
|
|
326
|
+
}
|
|
327
|
+
function padAnsi(value, width) {
|
|
328
|
+
const visible = stripAnsi(value).length;
|
|
329
|
+
return `${value}${" ".repeat(Math.max(0, width - visible))}`;
|
|
330
|
+
}
|
|
331
|
+
function stripAnsi(value) {
|
|
332
|
+
return value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
333
|
+
}
|
|
334
|
+
function style(text, color) {
|
|
335
|
+
if (!COLOR_ENABLED) {
|
|
336
|
+
return text;
|
|
337
|
+
}
|
|
338
|
+
return `${ANSI[color]}${text}${ANSI.reset}`;
|
|
339
|
+
}
|
|
340
|
+
function maskBarkUrl(url) {
|
|
341
|
+
try {
|
|
342
|
+
const parsed = new URL(url);
|
|
343
|
+
const pathParts = parsed.pathname.split("/").filter(Boolean);
|
|
344
|
+
const key = pathParts[pathParts.length - 1] ?? "";
|
|
345
|
+
const maskedKey = key.length <= 6 ? "***" : `${key.slice(0, 3)}...${key.slice(-3)}`;
|
|
346
|
+
return `${parsed.origin}/${maskedKey}`;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return url;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
main().catch((error) => {
|
|
353
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
354
|
+
process.exitCode = 1;
|
|
355
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pom-tool",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A small Node.js CLI for running Pomodoro timers in your terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pom": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"dev": "node --loader ts-node/esm src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pomodoro",
|
|
18
|
+
"cli",
|
|
19
|
+
"timer",
|
|
20
|
+
"bark"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "AlucPro",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/AlucPro/pom-tool.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/AlucPro/pom-tool",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/AlucPro/pom-tool/issues"
|
|
31
|
+
},
|
|
32
|
+
"packageManager": "pnpm@10.12.1",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.0.0",
|
|
38
|
+
"ts-node": "^10.9.2",
|
|
39
|
+
"typescript": "^5.8.2"
|
|
40
|
+
}
|
|
41
|
+
}
|