@arach/pomo 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +9 -3
  2. package/bin/pomo.js +128 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,9 +30,12 @@ pomo status
30
30
  Timer status [--json] · start · pause · toggle · reset · skip
31
31
  session <focus|short|long> · duration <minutes>
32
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>
33
+ Audio audio <url> · audio <play|pause|stop|next|prev>
34
+ audio session <focus|break|long> <favorite#|url|clear> · volume <0-100>
35
+ Video video <show|hide|toggle|page|player|browser>
36
+ Favorites fav · fav add <url> [title…] · fav rename <n> <title…>
37
+ fav url <n> <url> · fav move <from> <to>
38
+ fav set <json-file|json|-> · fav play <n> · fav remove <n> · fav clear
36
39
  Window show · hide · hud · menu · face <name> · settings · stats
37
40
  Login login · login import [--browser b] [--profile p] · login profiles
38
41
  login account <n> · logout
@@ -46,7 +49,10 @@ Run `pomo` with no arguments for a live status; `pomo help` for the full list.
46
49
  ```sh
47
50
  pomo intent "Writing the launch post"
48
51
  pomo audio "https://youtube.com/watch?v=jfKfPfyJRdk"
52
+ pomo audio session focus 1
49
53
  pomo fav play 1
54
+ pomo fav move 4 1
55
+ pomo fav set ./playlist.json
50
56
  pomo status --json | jq .remainingSeconds
51
57
  ```
52
58
 
package/bin/pomo.js CHANGED
@@ -13,6 +13,7 @@ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSy
13
13
  import { homedir, tmpdir } from 'node:os';
14
14
  import { join } from 'node:path';
15
15
  import { createInterface } from 'node:readline';
16
+ import { fileURLToPath } from 'node:url';
16
17
 
17
18
  const REPO = 'arach/pomo';
18
19
  const STATE_FILE = join(homedir(), 'Library', 'Application Support', 'Pomo', 'state.json');
@@ -31,11 +32,29 @@ function requireMac() {
31
32
  if (process.platform !== 'darwin') die('this CLI only works on macOS.');
32
33
  }
33
34
 
35
+ /**
36
+ * Which Pomo.app should handle a `pomo://` command. With many stale bundles
37
+ * registered (old worktrees, mounted DMGs, cached builds), bare `open pomo://`
38
+ * routes unpredictably to "some older version". So:
39
+ * 1. honour an explicit POMO_APP env override, else
40
+ * 2. when this CLI lives inside the repo, prefer its sibling dev build, so
41
+ * `pomo` drives the copy you're hacking on — not whatever LaunchServices
42
+ * happens to pick.
43
+ * Returns null for a plain npm install (no sibling app) → default routing.
44
+ */
45
+ function targetApp() {
46
+ if (process.env.POMO_APP) return process.env.POMO_APP;
47
+ const devApp = join(fileURLToPath(import.meta.url), '../../../macos/dist/Pomo.app');
48
+ return existsSync(devApp) ? devApp : null;
49
+ }
50
+
34
51
  /** Fire a pomo:// command at the app via `open`. */
35
52
  function send(path) {
36
53
  requireMac();
54
+ const url = `pomo://${path}`;
55
+ const app = targetApp();
37
56
  try {
38
- execFileSync('open', [`pomo://${path}`], { stdio: 'ignore' });
57
+ execFileSync('open', app ? ['-a', app, url] : [url], { stdio: 'ignore' });
39
58
  } catch {
40
59
  die(`couldn't reach Pomo. Is it installed? Try: pomo install`);
41
60
  }
@@ -97,6 +116,20 @@ function printStatus(args) {
97
116
  console.log(` hud ${s.hudVisible ? 'visible' : 'hidden'} · face ${s.watchface}`);
98
117
 
99
118
  const favs = s.favorites || [];
119
+ const sessionAudio = s.sessionAudioURLs || {};
120
+ const sessionLabels = [
121
+ ['focus', 'focus'],
122
+ ['shortBreak', 'break'],
123
+ ['longBreak', 'long'],
124
+ ];
125
+ const assigned = sessionLabels.filter(([key]) => sessionAudio[key]);
126
+ if (assigned.length) {
127
+ console.log(' session audio');
128
+ assigned.forEach(([key, label]) => {
129
+ const fav = favs.find((f) => f.url === sessionAudio[key]);
130
+ console.log(` ${label.padEnd(5)} ${fav ? fav.title : sessionAudio[key]}`);
131
+ });
132
+ }
100
133
  if (favs.length) {
101
134
  console.log(' favorites');
102
135
  favs.forEach((f, i) => console.log(` ${i + 1}. ${f.title}`));
@@ -301,6 +334,51 @@ function favorites(args) {
301
334
  const title = args.slice(2).join(' ') || undefined;
302
335
  return send(`favorite/add${query({ url, title })}`);
303
336
  }
337
+ case 'rename': {
338
+ const n = parseInt(args[1], 10);
339
+ const title = args.slice(2).join(' ');
340
+ if (!Number.isInteger(n) || !title) die('usage: pomo fav rename <n> <title…>');
341
+ return send(`favorite/update/${n}${query({ title })}`);
342
+ }
343
+ case 'url': {
344
+ const n = parseInt(args[1], 10);
345
+ const url = args[2];
346
+ if (!Number.isInteger(n) || !url) die('usage: pomo fav url <n> <url>');
347
+ return send(`favorite/update/${n}${query({ url })}`);
348
+ }
349
+ case 'move': {
350
+ const from = parseInt(args[1], 10);
351
+ const to = parseInt(args[2], 10);
352
+ if (!Number.isInteger(from) || !Number.isInteger(to)) die('usage: pomo fav move <from> <to>');
353
+ return send(`favorite/move/${from}/${to}`);
354
+ }
355
+ case 'set':
356
+ case 'replace': {
357
+ const source = args[1];
358
+ if (!source) die('usage: pomo fav set <json-file|json|->');
359
+ const raw = source === '-'
360
+ ? readFileSync(0, 'utf8')
361
+ : existsSync(source)
362
+ ? readFileSync(source, 'utf8')
363
+ : args.slice(1).join(' ');
364
+ let items;
365
+ try {
366
+ items = JSON.parse(raw);
367
+ } catch (error) {
368
+ die(`invalid favorites JSON: ${error.message}`);
369
+ }
370
+ if (!Array.isArray(items)) die('favorites JSON must be an array of { "title": "...", "url": "..." } objects');
371
+ const normalized = items.map((item, index) => {
372
+ if (!item || typeof item !== 'object') die(`favorite ${index + 1} must be an object`);
373
+ const url = String(item.url || '').trim();
374
+ if (!url) die(`favorite ${index + 1} is missing url`);
375
+ const title = String(item.title || '').trim();
376
+ return { title: title || url, url };
377
+ });
378
+ return send(`favorite/set${query({ items: JSON.stringify(normalized) })}`);
379
+ }
380
+ case 'clear':
381
+ return send('favorite/clear');
304
382
  case 'play': {
305
383
  const n = parseInt(args[1], 10);
306
384
  if (!Number.isInteger(n)) die('usage: pomo fav play <n>');
@@ -316,6 +394,43 @@ function favorites(args) {
316
394
  }
317
395
  }
318
396
 
397
+ function sessionKey(input) {
398
+ switch ((input || '').toLowerCase()) {
399
+ case 'focus':
400
+ case 'work':
401
+ return 'focus';
402
+ case 'short':
403
+ case 'break':
404
+ case 'shortbreak':
405
+ case 'short-break':
406
+ return 'break';
407
+ case 'long':
408
+ case 'longbreak':
409
+ case 'long-break':
410
+ return 'long';
411
+ default:
412
+ return null;
413
+ }
414
+ }
415
+
416
+ function sessionAudio(args) {
417
+ const type = sessionKey(args[0]);
418
+ if (!type) die('usage: pomo audio session <focus|break|long> <favorite#|url|clear>');
419
+
420
+ const value = args[1];
421
+ if (!value) die('usage: pomo audio session <focus|break|long> <favorite#|url|clear>');
422
+ if (value.toLowerCase() === 'clear') return send(`audio/session/${type}/clear`);
423
+
424
+ let url = value;
425
+ if (/^\d+$/.test(value)) {
426
+ const favorite = (readState().favorites || [])[Number(value) - 1];
427
+ if (!favorite) die(`no favorite #${value}`);
428
+ url = favorite.url;
429
+ }
430
+ if (!/^https?:\/\//i.test(url)) die('session audio must be a favorite number, URL, or clear');
431
+ return send(`audio/session/${type}${query({ url })}`);
432
+ }
433
+
319
434
  // ─── help ─────────────────────────────────────────────────────────────────
320
435
 
321
436
  function help() {
@@ -336,14 +451,20 @@ Intent
336
451
  Audio / video
337
452
  audio <url> play a YouTube/stream link
338
453
  audio <play|pause|stop|next|prev>
454
+ audio session <focus|break|long> <favorite#|url|clear>
339
455
  volume <0-100>
340
- video <show|hide|toggle|browser>
456
+ video <show|hide|toggle|page|player|browser>
341
457
 
342
458
  Favorites
343
459
  fav list saved stations
344
460
  fav add <url> [title…]
461
+ fav rename <n> <title…>
462
+ fav url <n> <url>
463
+ fav move <from> <to>
464
+ fav set <json-file|json|->
345
465
  fav play <n>
346
466
  fav remove <n>
467
+ fav clear
347
468
 
348
469
  Window & app
349
470
  show | hide | hud summon / dismiss / toggle the HUD
@@ -414,10 +535,11 @@ switch (cmd) {
414
535
 
415
536
  case 'audio': {
416
537
  const a = rest[0] || '';
417
- if (/^https?:\/\//i.test(a)) send(`audio${query({ url: a })}`);
538
+ if (a === 'session' || a === 'for') sessionAudio(rest.slice(1));
539
+ else if (/^https?:\/\//i.test(a)) send(`audio${query({ url: a })}`);
418
540
  else if (['play', 'pause', 'stop', 'next', 'prev', 'previous'].includes(a.toLowerCase()))
419
541
  send(`audio/${a.toLowerCase()}`);
420
- else die('usage: pomo audio <url|play|pause|stop|next|prev>');
542
+ else die('usage: pomo audio <url|play|pause|stop|next|prev|session>');
421
543
  break;
422
544
  }
423
545
 
@@ -428,8 +550,8 @@ switch (cmd) {
428
550
 
429
551
  case 'video': {
430
552
  const sub = (rest[0] || 'toggle').toLowerCase();
431
- if (!['show', 'hide', 'toggle', 'browser', 'open'].includes(sub))
432
- die('usage: pomo video <show|hide|toggle|browser>');
553
+ if (!['show', 'hide', 'toggle', 'page', 'full', 'original', 'expand', 'player', 'bare', 'screen', 'collapse', 'browser', 'open'].includes(sub))
554
+ die('usage: pomo video <show|hide|toggle|page|player|browser>');
433
555
  send(`video/${sub}`);
434
556
  break;
435
557
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arach/pomo",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
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
5
  "type": "module",
6
6
  "bin": {