@arach/pomo 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/README.md +66 -0
- package/bin/pomo.js +465 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @arach/pomo
|
|
2
|
+
|
|
3
|
+
Control — and install — the [Pomo](https://github.com/arach/pomo) macOS HUD
|
|
4
|
+
timer from the shell or an agent. A thin, zero-dependency wrapper over Pomo's
|
|
5
|
+
`pomo://` URL scheme and the JSON state file it writes on every tick.
|
|
6
|
+
|
|
7
|
+
> macOS only. It drives the installed Pomo app via `open` (and `hdiutil` for
|
|
8
|
+
> `install`); it doesn't bundle the app itself.
|
|
9
|
+
|
|
10
|
+
## Use it
|
|
11
|
+
|
|
12
|
+
No install needed — run it with `npx`:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npx @arach/pomo install # download & install the latest Pomo.app
|
|
16
|
+
npx @arach/pomo start # start a focus session
|
|
17
|
+
npx @arach/pomo status # see what's happening
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or put it on your PATH:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g @arach/pomo
|
|
24
|
+
pomo status
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Timer status [--json] · start · pause · toggle · reset · skip
|
|
31
|
+
session <focus|short|long> · duration <minutes>
|
|
32
|
+
Intent intent <text…> · intent clear
|
|
33
|
+
Audio audio <url> · audio <play|pause|stop|next|prev> · volume <0-100>
|
|
34
|
+
Video video <show|hide|toggle|browser>
|
|
35
|
+
Favorites fav · fav add <url> [title…] · fav play <n> · fav remove <n>
|
|
36
|
+
Window show · hide · hud · menu · face <name> · settings · stats
|
|
37
|
+
Login login · login import [--browser b] [--profile p] · login profiles
|
|
38
|
+
login account <n> · logout
|
|
39
|
+
App install [--dry-run] [--open] · quit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Run `pomo` with no arguments for a live status; `pomo help` for the full list.
|
|
43
|
+
|
|
44
|
+
### Examples
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pomo intent "Writing the launch post"
|
|
48
|
+
pomo audio "https://youtube.com/watch?v=jfKfPfyJRdk"
|
|
49
|
+
pomo fav play 1
|
|
50
|
+
pomo status --json | jq .remainingSeconds
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `install`
|
|
54
|
+
|
|
55
|
+
Finds the newest GitHub release carrying a `.dmg`, downloads it, mounts it,
|
|
56
|
+
copies `Pomo.app` into `/Applications` (falling back to `~/Applications` if that
|
|
57
|
+
isn't writable), clears the download quarantine, and unmounts. `--dry-run`
|
|
58
|
+
prints what it would do; `--open` launches the app afterward.
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
- **Commands** → `open "pomo://<verb>"` (fire-and-forget).
|
|
63
|
+
- **`status`** → reads `~/Library/Application Support/Pomo/state.json`.
|
|
64
|
+
|
|
65
|
+
That's the whole contract, so anything the app exposes over `pomo://` is one
|
|
66
|
+
line away here.
|
package/bin/pomo.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pomo — control and install the Pomo macOS HUD timer.
|
|
3
|
+
//
|
|
4
|
+
// Commands are sent fire-and-forget over the `pomo://` URL scheme (via `open`);
|
|
5
|
+
// `status` reads the JSON state file the app writes on every tick; `install`
|
|
6
|
+
// pulls the latest .dmg from GitHub releases and drops Pomo.app in /Applications.
|
|
7
|
+
//
|
|
8
|
+
// Zero dependencies — Node 18+ built-ins only. macOS only (it drives `open`,
|
|
9
|
+
// `hdiutil`, etc.).
|
|
10
|
+
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir, tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
|
|
17
|
+
const REPO = 'arach/pomo';
|
|
18
|
+
const STATE_FILE = join(homedir(), 'Library', 'Application Support', 'Pomo', 'state.json');
|
|
19
|
+
|
|
20
|
+
const argv = process.argv.slice(2);
|
|
21
|
+
const cmd = (argv[0] || '').toLowerCase();
|
|
22
|
+
|
|
23
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function die(msg) {
|
|
26
|
+
console.error(`pomo: ${msg}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireMac() {
|
|
31
|
+
if (process.platform !== 'darwin') die('this CLI only works on macOS.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Fire a pomo:// command at the app via `open`. */
|
|
35
|
+
function send(path) {
|
|
36
|
+
requireMac();
|
|
37
|
+
try {
|
|
38
|
+
execFileSync('open', [`pomo://${path}`], { stdio: 'ignore' });
|
|
39
|
+
} catch {
|
|
40
|
+
die(`couldn't reach Pomo. Is it installed? Try: pomo install`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build a query string from {k: v} pairs, skipping null/undefined. */
|
|
45
|
+
function query(pairs) {
|
|
46
|
+
const parts = Object.entries(pairs)
|
|
47
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
48
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`);
|
|
49
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Pull a `--flag value` (or `--flag=value`) out of args; returns the value or undefined. */
|
|
53
|
+
function takeFlag(args, name) {
|
|
54
|
+
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
55
|
+
if (eq) return eq.slice(name.length + 3);
|
|
56
|
+
const i = args.indexOf(`--${name}`);
|
|
57
|
+
if (i >= 0 && i + 1 < args.length) return args[i + 1];
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasFlag(args, name) {
|
|
62
|
+
return args.includes(`--${name}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readState() {
|
|
66
|
+
if (!existsSync(STATE_FILE)) {
|
|
67
|
+
die(`no state file at ${STATE_FILE}\n Pomo may not be installed or hasn't run yet. Try: pomo install`);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
die('state file is unreadable.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── status ───────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function printStatus(args) {
|
|
79
|
+
const s = readState();
|
|
80
|
+
if (hasFlag(args, 'json')) {
|
|
81
|
+
console.log(JSON.stringify(s, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const dot = s.phase === 'running' ? '●' : s.phase === 'paused' ? '❚❚' : '○';
|
|
85
|
+
const phase = s.phase === 'idle' ? 'idle' : s.phase;
|
|
86
|
+
console.log(`${dot} ${s.sessionType} · ${s.clock} (${phase})`);
|
|
87
|
+
if (s.intent) console.log(` intent ${s.intent}`);
|
|
88
|
+
|
|
89
|
+
let audio = '—';
|
|
90
|
+
if (s.audioURL) {
|
|
91
|
+
const fav = (s.favorites || []).find((f) => f.url === s.audioURL);
|
|
92
|
+
const label = fav ? fav.title : s.audioURL;
|
|
93
|
+
audio = `${s.audioPlaying ? '▶' : '⏸'} ${label}`;
|
|
94
|
+
}
|
|
95
|
+
console.log(` audio ${audio}`);
|
|
96
|
+
console.log(` today ${s.focusToday ?? 0} focus · streak ${s.streakDays ?? 0}d · ${s.focusTotal ?? 0} total`);
|
|
97
|
+
console.log(` hud ${s.hudVisible ? 'visible' : 'hidden'} · face ${s.watchface}`);
|
|
98
|
+
|
|
99
|
+
const favs = s.favorites || [];
|
|
100
|
+
if (favs.length) {
|
|
101
|
+
console.log(' favorites');
|
|
102
|
+
favs.forEach((f, i) => console.log(` ${i + 1}. ${f.title}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── install ────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async function latestDmg() {
|
|
109
|
+
const res = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=30`, {
|
|
110
|
+
headers: { 'User-Agent': 'pomo-cli', Accept: 'application/vnd.github+json' },
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) die(`GitHub API returned ${res.status} ${res.statusText}.`);
|
|
113
|
+
const releases = await res.json();
|
|
114
|
+
// Releases come back newest-first; take the newest one carrying a .dmg.
|
|
115
|
+
for (const rel of releases) {
|
|
116
|
+
if (rel.draft) continue;
|
|
117
|
+
const dmgs = (rel.assets || []).filter((a) => a.name.toLowerCase().endsWith('.dmg'));
|
|
118
|
+
if (!dmgs.length) continue;
|
|
119
|
+
// Prefer the native, unversioned `Pomo.dmg`; otherwise take the first .dmg.
|
|
120
|
+
const asset = dmgs.find((a) => a.name === 'Pomo.dmg') || dmgs[0];
|
|
121
|
+
return { tag: rel.tag_name, name: rel.name, asset };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sh(file, args) {
|
|
127
|
+
return execFileSync(file, args, { encoding: 'utf8' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function install(args) {
|
|
131
|
+
requireMac();
|
|
132
|
+
const dryRun = hasFlag(args, 'dry-run');
|
|
133
|
+
|
|
134
|
+
process.stderr.write('Finding the latest Pomo .dmg…\n');
|
|
135
|
+
const found = await latestDmg();
|
|
136
|
+
if (!found) die(`no .dmg asset found in any ${REPO} release yet.`);
|
|
137
|
+
const { tag, asset } = found;
|
|
138
|
+
const sizeMB = (asset.size / 1e6).toFixed(1);
|
|
139
|
+
console.log(`Latest: ${asset.name} (${tag}, ${sizeMB} MB)`);
|
|
140
|
+
console.log(` ${asset.browser_download_url}`);
|
|
141
|
+
|
|
142
|
+
if (dryRun) {
|
|
143
|
+
console.log(' → would install to /Applications/Pomo.app (dry run, nothing downloaded)');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 1. Download.
|
|
148
|
+
const dir = mkdtempSync(join(tmpdir(), 'pomo-install-'));
|
|
149
|
+
const dmgPath = join(dir, asset.name);
|
|
150
|
+
process.stderr.write('Downloading…\n');
|
|
151
|
+
const dl = await fetch(asset.browser_download_url, { headers: { 'User-Agent': 'pomo-cli' } });
|
|
152
|
+
if (!dl.ok) die(`download failed: ${dl.status} ${dl.statusText}`);
|
|
153
|
+
writeFileSync(dmgPath, Buffer.from(await dl.arrayBuffer()));
|
|
154
|
+
|
|
155
|
+
// 2. Mount.
|
|
156
|
+
process.stderr.write('Mounting…\n');
|
|
157
|
+
const attach = sh('hdiutil', ['attach', dmgPath, '-nobrowse', '-noverify', '-readonly']);
|
|
158
|
+
const mount = attach
|
|
159
|
+
.trim()
|
|
160
|
+
.split('\n')
|
|
161
|
+
.map((l) => l.split('\t').pop().trim())
|
|
162
|
+
.filter((p) => p.startsWith('/Volumes/'))
|
|
163
|
+
.pop();
|
|
164
|
+
if (!mount) die('could not determine the mounted volume.');
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const app = readdirSync(mount).find((n) => n.endsWith('.app'));
|
|
168
|
+
if (!app) die('no .app inside the disk image.');
|
|
169
|
+
|
|
170
|
+
// 3. Copy into /Applications (fall back to ~/Applications if not writable).
|
|
171
|
+
let appsDir = '/Applications';
|
|
172
|
+
let dest = join(appsDir, app);
|
|
173
|
+
try {
|
|
174
|
+
rmSync(dest, { recursive: true, force: true });
|
|
175
|
+
sh('ditto', [join(mount, app), dest]);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (e && (e.code === 'EACCES' || e.code === 'EPERM' || /Permission denied/.test(String(e)))) {
|
|
178
|
+
appsDir = join(homedir(), 'Applications');
|
|
179
|
+
dest = join(appsDir, app);
|
|
180
|
+
process.stderr.write('/Applications not writable — installing to ~/Applications instead.\n');
|
|
181
|
+
rmSync(dest, { recursive: true, force: true });
|
|
182
|
+
sh('mkdir', ['-p', appsDir]);
|
|
183
|
+
sh('ditto', [join(mount, app), dest]);
|
|
184
|
+
} else {
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 4. Clear the download quarantine so it launches without a Gatekeeper prompt.
|
|
190
|
+
try {
|
|
191
|
+
sh('xattr', ['-dr', 'com.apple.quarantine', dest]);
|
|
192
|
+
} catch {
|
|
193
|
+
/* best effort */
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(`Installed ${app} → ${dest}`);
|
|
197
|
+
if (hasFlag(args, 'open')) {
|
|
198
|
+
sh('open', [dest]);
|
|
199
|
+
console.log('Launched.');
|
|
200
|
+
} else {
|
|
201
|
+
console.log('Launch it with: pomo show (or: open -a Pomo)');
|
|
202
|
+
}
|
|
203
|
+
} finally {
|
|
204
|
+
// 5. Always unmount.
|
|
205
|
+
try {
|
|
206
|
+
sh('hdiutil', ['detach', mount, '-quiet']);
|
|
207
|
+
} catch {
|
|
208
|
+
/* best effort */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── login / cookie import (local browser data) ─────────────────────────────
|
|
214
|
+
|
|
215
|
+
/** Detected Chromium-family browser profiles, for cookie import. */
|
|
216
|
+
function browserProfiles() {
|
|
217
|
+
// Most-common first, so the browsers people actually use lead the picker.
|
|
218
|
+
const bases = [
|
|
219
|
+
['chrome', 'Google/Chrome'],
|
|
220
|
+
['edge', 'Microsoft Edge'],
|
|
221
|
+
['brave', 'BraveSoftware/Brave-Browser'],
|
|
222
|
+
['chromium', 'Chromium'],
|
|
223
|
+
];
|
|
224
|
+
const out = [];
|
|
225
|
+
for (const [browser, base] of bases) {
|
|
226
|
+
const ls = join(homedir(), 'Library', 'Application Support', base, 'Local State');
|
|
227
|
+
if (!existsSync(ls)) continue;
|
|
228
|
+
try {
|
|
229
|
+
const cache = JSON.parse(readFileSync(ls, 'utf8'))?.profile?.info_cache ?? {};
|
|
230
|
+
for (const [dir, info] of Object.entries(cache)) {
|
|
231
|
+
out.push({ browser, dir, name: info?.name ?? '?' });
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
/* skip unreadable */
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function listProfiles() {
|
|
241
|
+
const ps = browserProfiles();
|
|
242
|
+
if (!ps.length) return console.log('(no Chromium browser profiles found)');
|
|
243
|
+
for (const p of ps) console.log(` ${p.browser.padEnd(9)} ${p.dir.padEnd(14)} ${p.name}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function pickProfile(ps) {
|
|
247
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
248
|
+
ps.forEach((p, i) => process.stderr.write(` ${i + 1}. ${p.browser} · ${p.name} (${p.dir})\n`));
|
|
249
|
+
const ans = await new Promise((res) => rl.question('Import cookies from which profile? [number] ', res));
|
|
250
|
+
rl.close();
|
|
251
|
+
return ps[parseInt(ans, 10) - 1];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function loginCmd(args) {
|
|
255
|
+
const sub = (args[0] || '').toLowerCase();
|
|
256
|
+
switch (sub) {
|
|
257
|
+
case 'profiles':
|
|
258
|
+
return listProfiles();
|
|
259
|
+
case 'account': {
|
|
260
|
+
const n = parseInt(args[1], 10);
|
|
261
|
+
if (!Number.isInteger(n)) die('usage: pomo login account <n>');
|
|
262
|
+
return send(`login/account/${n}`);
|
|
263
|
+
}
|
|
264
|
+
case 'import': {
|
|
265
|
+
let browser = takeFlag(args, 'browser');
|
|
266
|
+
let profile = takeFlag(args, 'profile');
|
|
267
|
+
if (!browser) {
|
|
268
|
+
const ps = browserProfiles();
|
|
269
|
+
if (!ps.length) die('no Chromium browser profiles found to import from.');
|
|
270
|
+
if (!process.stdin.isTTY) {
|
|
271
|
+
console.log('Pick one and pass --browser/--profile:');
|
|
272
|
+
listProfiles();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const choice = await pickProfile(ps);
|
|
276
|
+
if (!choice) die('no profile selected.');
|
|
277
|
+
browser = choice.browser;
|
|
278
|
+
profile = choice.dir;
|
|
279
|
+
}
|
|
280
|
+
return send(`login/import${query({ browser, profile })}`);
|
|
281
|
+
}
|
|
282
|
+
default:
|
|
283
|
+
return send('login');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ─── favorites (list reads state; mutations go over the scheme) ──────────────
|
|
288
|
+
|
|
289
|
+
function favorites(args) {
|
|
290
|
+
const sub = (args[0] || 'list').toLowerCase();
|
|
291
|
+
switch (sub) {
|
|
292
|
+
case 'list': {
|
|
293
|
+
const favs = readState().favorites || [];
|
|
294
|
+
if (!favs.length) return console.log('(no favorites yet)');
|
|
295
|
+
favs.forEach((f, i) => console.log(`${i + 1}. ${f.title} — ${f.url}`));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
case 'add': {
|
|
299
|
+
const url = args[1];
|
|
300
|
+
if (!url) die('usage: pomo fav add <url> [title…]');
|
|
301
|
+
const title = args.slice(2).join(' ') || undefined;
|
|
302
|
+
return send(`favorite/add${query({ url, title })}`);
|
|
303
|
+
}
|
|
304
|
+
case 'play': {
|
|
305
|
+
const n = parseInt(args[1], 10);
|
|
306
|
+
if (!Number.isInteger(n)) die('usage: pomo fav play <n>');
|
|
307
|
+
return send(`favorite/play/${n}`);
|
|
308
|
+
}
|
|
309
|
+
case 'remove': {
|
|
310
|
+
const n = parseInt(args[1], 10);
|
|
311
|
+
if (!Number.isInteger(n)) die('usage: pomo fav remove <n>');
|
|
312
|
+
return send(`favorite/remove/${n}`);
|
|
313
|
+
}
|
|
314
|
+
default:
|
|
315
|
+
die(`unknown fav command: ${sub}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── help ─────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
function help() {
|
|
322
|
+
console.log(`pomo — control & install the Pomo macOS HUD timer
|
|
323
|
+
|
|
324
|
+
Usage: pomo <command> [args]
|
|
325
|
+
|
|
326
|
+
Timer
|
|
327
|
+
status [--json] show the live state (default when run with no command)
|
|
328
|
+
start | pause | toggle | reset | skip
|
|
329
|
+
session <focus|short|long>
|
|
330
|
+
duration <minutes>
|
|
331
|
+
|
|
332
|
+
Intent
|
|
333
|
+
intent <text…> set what you're working on
|
|
334
|
+
intent clear clear it
|
|
335
|
+
|
|
336
|
+
Audio / video
|
|
337
|
+
audio <url> play a YouTube/stream link
|
|
338
|
+
audio <play|pause|stop|next|prev>
|
|
339
|
+
volume <0-100>
|
|
340
|
+
video <show|hide|toggle|browser>
|
|
341
|
+
|
|
342
|
+
Favorites
|
|
343
|
+
fav list saved stations
|
|
344
|
+
fav add <url> [title…]
|
|
345
|
+
fav play <n>
|
|
346
|
+
fav remove <n>
|
|
347
|
+
|
|
348
|
+
Window & app
|
|
349
|
+
show | hide | hud summon / dismiss / toggle the HUD
|
|
350
|
+
menu open the menu-bar popover
|
|
351
|
+
face <name> switch watchface
|
|
352
|
+
settings | stats open the Settings / Stats window
|
|
353
|
+
login audio sign-in (YouTube)
|
|
354
|
+
login import [--browser b] [--profile p] import browser cookies (ad-free)
|
|
355
|
+
login profiles list detected browser profiles
|
|
356
|
+
login account <n> | logout
|
|
357
|
+
install [--dry-run] [--open] download & install the latest .dmg
|
|
358
|
+
quit
|
|
359
|
+
|
|
360
|
+
Reads ~/Library/Application Support/Pomo/state.json; sends pomo:// URLs via open.`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── dispatch ────────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
const rest = argv.slice(1);
|
|
366
|
+
|
|
367
|
+
switch (cmd) {
|
|
368
|
+
// plain verbs
|
|
369
|
+
case 'start':
|
|
370
|
+
case 'pause':
|
|
371
|
+
case 'toggle':
|
|
372
|
+
case 'reset':
|
|
373
|
+
case 'skip':
|
|
374
|
+
case 'menu':
|
|
375
|
+
case 'show':
|
|
376
|
+
case 'hide':
|
|
377
|
+
case 'logout':
|
|
378
|
+
case 'settings':
|
|
379
|
+
case 'stats':
|
|
380
|
+
case 'quit':
|
|
381
|
+
send(cmd);
|
|
382
|
+
break;
|
|
383
|
+
|
|
384
|
+
case 'login':
|
|
385
|
+
await loginCmd(rest);
|
|
386
|
+
break;
|
|
387
|
+
|
|
388
|
+
case 'hud':
|
|
389
|
+
send('hud');
|
|
390
|
+
break;
|
|
391
|
+
|
|
392
|
+
case 'session':
|
|
393
|
+
if (!rest[0]) die('usage: pomo session <focus|short|long>');
|
|
394
|
+
send(`session/${rest[0].toLowerCase()}`);
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case 'face':
|
|
398
|
+
if (!rest[0]) die('usage: pomo face <name>');
|
|
399
|
+
send(`face/${rest[0].toLowerCase()}`);
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case 'duration':
|
|
403
|
+
if (!/^\d+$/.test(rest[0] || '')) die('usage: pomo duration <minutes>');
|
|
404
|
+
send(`duration/${rest[0]}`);
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
case 'intent':
|
|
408
|
+
if (rest[0] === 'clear' || hasFlag(rest, 'clear') || rest.length === 0) {
|
|
409
|
+
send('intent/clear');
|
|
410
|
+
} else {
|
|
411
|
+
send(`intent${query({ text: rest.join(' ') })}`);
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case 'audio': {
|
|
416
|
+
const a = rest[0] || '';
|
|
417
|
+
if (/^https?:\/\//i.test(a)) send(`audio${query({ url: a })}`);
|
|
418
|
+
else if (['play', 'pause', 'stop', 'next', 'prev', 'previous'].includes(a.toLowerCase()))
|
|
419
|
+
send(`audio/${a.toLowerCase()}`);
|
|
420
|
+
else die('usage: pomo audio <url|play|pause|stop|next|prev>');
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
case 'volume':
|
|
425
|
+
if (!/^\d+$/.test(rest[0] || '')) die('usage: pomo volume <0-100>');
|
|
426
|
+
send(`volume/${rest[0]}`);
|
|
427
|
+
break;
|
|
428
|
+
|
|
429
|
+
case 'video': {
|
|
430
|
+
const sub = (rest[0] || 'toggle').toLowerCase();
|
|
431
|
+
if (!['show', 'hide', 'toggle', 'browser', 'open'].includes(sub))
|
|
432
|
+
die('usage: pomo video <show|hide|toggle|browser>');
|
|
433
|
+
send(`video/${sub}`);
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
case 'fav':
|
|
438
|
+
case 'favorite':
|
|
439
|
+
case 'favorites':
|
|
440
|
+
favorites(rest);
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case 'status':
|
|
444
|
+
printStatus(rest);
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
case 'install':
|
|
448
|
+
await install(rest);
|
|
449
|
+
break;
|
|
450
|
+
|
|
451
|
+
case '':
|
|
452
|
+
// bare `pomo` → a friendly status if the app's around, else help.
|
|
453
|
+
if (existsSync(STATE_FILE)) printStatus([]);
|
|
454
|
+
else help();
|
|
455
|
+
break;
|
|
456
|
+
|
|
457
|
+
case 'help':
|
|
458
|
+
case '--help':
|
|
459
|
+
case '-h':
|
|
460
|
+
help();
|
|
461
|
+
break;
|
|
462
|
+
|
|
463
|
+
default:
|
|
464
|
+
die(`unknown command: ${cmd}\n Run 'pomo help' for usage.`);
|
|
465
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arach/pomo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Control and install the Pomo macOS HUD timer from the shell or an agent — a thin wrapper over the pomo:// URL scheme and the JSON state file.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pomo": "bin/pomo.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"os": [
|
|
17
|
+
"darwin"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "arach",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/arach/pomo.git",
|
|
24
|
+
"directory": "apps/cli"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/arach/pomo/tree/master/apps/cli",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"pomodoro",
|
|
29
|
+
"pomo",
|
|
30
|
+
"cli",
|
|
31
|
+
"macos",
|
|
32
|
+
"timer",
|
|
33
|
+
"focus",
|
|
34
|
+
"agent"
|
|
35
|
+
]
|
|
36
|
+
}
|