@beauraines/rtm-cli 1.12.0 → 1.13.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [1.13.0](https://github.com/beauraines/rtm-cli/compare/v1.12.0...v1.13.0) (2025-12-17)
6
+
7
+
8
+ ### Features
9
+
10
+ * **task:** human readeable recurrence and estimates ([#163](https://github.com/beauraines/rtm-cli/issues/163)) ([d78dd0a](https://github.com/beauraines/rtm-cli/commit/d78dd0a1204584a30df2f8a568c25142d93b36cc))
11
+
5
12
  ## [1.12.0](https://github.com/beauraines/rtm-cli/compare/v1.11.0...v1.12.0) (2025-12-17)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beauraines/rtm-cli",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "RTM CLI",
5
5
  "keywords": [
6
6
  "rtm",
@@ -44,9 +44,11 @@
44
44
  "dateformat": "^4.0.0",
45
45
  "debug": "^4.3.4",
46
46
  "deepmerge": "^4.0.0",
47
+ "iso8601-duration": "^2.1.3",
47
48
  "open": "^8.4.2",
48
49
  "ora": "^5.0.0",
49
50
  "prompt-sync": "^4.2.0",
51
+ "rrule": "^2.8.1",
50
52
  "should-release": "^1.2.0",
51
53
  "standard-version": "^9.5.0",
52
54
  "window-size": "^1.1.0"
package/src/cmd/task.js CHANGED
@@ -6,6 +6,7 @@ const finish = require('../utils/finish.js');
6
6
  const filter = require('../utils/filter');
7
7
  const { indexPrompt } = require('../utils/prompt')
8
8
  const debug = require('debug')('rtm-cli-task');
9
+ const { humanizeDuration, humanizeRecurrence } = require('../utils/format');
9
10
 
10
11
 
11
12
  let TASKS = [];
@@ -93,7 +94,7 @@ function displayTask(taskDetails) {
93
94
  debug(taskDetails)
94
95
  let index = taskDetails.index;
95
96
  // eslint-disable-next-line no-unused-vars
96
- const { _list, list_id, taskseries_id, task_id, _index, name, priority, start, due, completed, isRecurring, isSubtask, estimate, url, tags, notes ,...otherAttributes } = taskDetails.task;
97
+ const { _list, list_id, taskseries_id, task_id, _index, name, priority, start, due, completed, isRecurring, recurrenceRuleRaw, isSubtask, estimate, url, tags, notes, ...otherAttributes } = taskDetails.task;
97
98
 
98
99
  const listName = LIST_MAP.get(list_id) || "Not found";
99
100
 
@@ -112,10 +113,15 @@ function displayTask(taskDetails) {
112
113
  log(`${completed}`)
113
114
  log.style(`Is Recurring: `,styles.index)
114
115
  log(`${isRecurring}`)
116
+ if (isRecurring && recurrenceRuleRaw) {
117
+ log.style(`Recurrence: `,styles.index)
118
+ log(humanizeRecurrence(recurrenceRuleRaw))
119
+ }
120
+
115
121
  log.style(`Is Subtask: `,styles.index)
116
122
  log(`${isSubtask}`)
117
123
  log.style(`Estimate: `,styles.index)
118
- log(`${estimate}`)
124
+ log(humanizeDuration(estimate))
119
125
  log.style(`Url: `,styles.index)
120
126
  log(`${url}`)
121
127
  log.style(`Tags: `,styles.index)
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const { humanizeDuration, humanizeRecurrence } = require('../utils/format');
4
+
5
+ describe('humanizeDuration', () => {
6
+ test('parses hours and minutes', () => {
7
+ expect(humanizeDuration('PT1H30M')).toBe('1 hour 30 minutes');
8
+ });
9
+
10
+ test('parses days and seconds', () => {
11
+ expect(humanizeDuration('P2DT15S')).toBe('2 days 15 seconds');
12
+ });
13
+
14
+ test('returns input for invalid strings', () => {
15
+ expect(humanizeDuration('invalid')).toBe('invalid');
16
+ });
17
+
18
+ test('returns empty for non-string', () => {
19
+ expect(humanizeDuration(123)).toBe('');
20
+ });
21
+ });
22
+
23
+ describe('humanizeRecurrence', () => {
24
+ test('parses daily recurrence', () => {
25
+ const rawRule = { $t: 'FREQ=DAILY;INTERVAL=1' };
26
+ expect(humanizeRecurrence(rawRule)).toMatch(/every day/i);
27
+ });
28
+
29
+ test('parses weekly recurrence with interval', () => {
30
+ const rawRule = { $t: 'FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE' };
31
+ const result = humanizeRecurrence(rawRule);
32
+ expect(result).toMatch(/every 2 weeks on Monday, Wednesday/i);
33
+ });
34
+
35
+ test('parses default weekly recurrence', () => {
36
+ const rawRule = { every: '0', $t: 'FREQ=WEEKLY;INTERVAL=1;WKST=SU' };
37
+ expect(humanizeRecurrence(rawRule)).toMatch(/every week/i);
38
+ });
39
+
40
+ test('returns raw for invalid rule', () => {
41
+ const rawRule = { $t: 'invalid' };
42
+ expect(humanizeRecurrence(rawRule)).toBe('invalid');
43
+ });
44
+
45
+ test('empty when no $t', () => {
46
+ expect(humanizeRecurrence({ every: '1' })).toBe('');
47
+ });
48
+ });
49
+
50
+ describe('humanizeRecurrence additional input types', () => {
51
+ test('parses recurrence from stringified JSON', () => {
52
+ const str = '{"$t":"FREQ=WEEKLY;INTERVAL=1;WKST=SU"}';
53
+ expect(humanizeRecurrence(str)).toMatch(/every week/i);
54
+ });
55
+
56
+ test('parses recurrence from raw rule string', () => {
57
+ const raw = 'FREQ=DAILY;INTERVAL=1';
58
+ expect(humanizeRecurrence(raw)).toMatch(/every day/i);
59
+ });
60
+
61
+ test('returns empty for non-rule string without JSON', () => {
62
+ expect(humanizeRecurrence('not a rule')).toBe('');
63
+ });
64
+ });
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const { RRule } = require('rrule');
4
+ const { parse: parseIso } = require('iso8601-duration');
5
+
6
+ /**
7
+ * Convert an ISO8601 duration string (e.g. "PT1H30M") into a human-readable string.
8
+ * @param {string} iso
9
+ * @returns {string}
10
+ */
11
+ function humanizeDuration(iso) {
12
+ if (typeof iso !== 'string') return '';
13
+ let dur;
14
+ try {
15
+ dur = parseIso(iso);
16
+ } catch (e) {
17
+ return iso;
18
+ }
19
+ const parts = [];
20
+ if (dur.years) parts.push(`${dur.years} year${dur.years > 1 ? 's' : ''}`);
21
+ if (dur.months) parts.push(`${dur.months} month${dur.months > 1 ? 's' : ''}`);
22
+ if (dur.days) parts.push(`${dur.days} day${dur.days > 1 ? 's' : ''}`);
23
+ if (dur.hours) parts.push(`${dur.hours} hour${dur.hours > 1 ? 's' : ''}`);
24
+ if (dur.minutes) parts.push(`${dur.minutes} minute${dur.minutes > 1 ? 's' : ''}`);
25
+ if (dur.seconds) parts.push(`${dur.seconds} second${dur.seconds > 1 ? 's' : ''}`);
26
+ return parts.length ? parts.join(' ') : iso;
27
+ }
28
+
29
+ /**
30
+ * Convert a recurrence rule object with property $t (RFC5545 string) into a human-readable string.
31
+ * @param {object} rawRule
32
+ * @returns {string}
33
+ */
34
+ function humanizeRecurrence(input) {
35
+ let ruleObj;
36
+ if (typeof input === 'string') {
37
+ // Try to parse JSON string
38
+ try {
39
+ ruleObj = JSON.parse(input);
40
+ } catch (e) {
41
+ // Not JSON: maybe raw RRULE string
42
+ if (input.includes('FREQ=')) {
43
+ try {
44
+ return RRule.fromString(input).toText();
45
+ } catch (e) {
46
+ return input;
47
+ }
48
+ }
49
+ return '';
50
+ }
51
+ } else if (typeof input === 'object' && input !== null) {
52
+ ruleObj = input;
53
+ } else {
54
+ return '';
55
+ }
56
+
57
+ const ruleString = ruleObj.$t;
58
+ if (typeof ruleString !== 'string') {
59
+ return '';
60
+ }
61
+ try {
62
+ return RRule.fromString(ruleString).toText();
63
+ } catch (e) {
64
+ return ruleString;
65
+ }
66
+ }
67
+
68
+ module.exports = { humanizeDuration, humanizeRecurrence };