@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 +7 -0
- package/package.json +3 -1
- package/src/cmd/task.js +8 -2
- package/src/tests/format.test.js +64 -0
- package/src/utils/format.js +68 -0
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.
|
|
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
|
|
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(
|
|
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 };
|