@beauraines/rtm-cli 1.11.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 +14 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/cmd/task.js +9 -2
- package/src/tests/format.test.js +64 -0
- package/src/utils/format.js +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
|
|
12
|
+
## [1.12.0](https://github.com/beauraines/rtm-cli/compare/v1.11.0...v1.12.0) (2025-12-17)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **task:** adds tasks as alias for task command ([#162](https://github.com/beauraines/rtm-cli/issues/162)) ([b9196ee](https://github.com/beauraines/rtm-cli/commit/b9196ee2d73680cce9ab01e39cd73e27b426fb7e)), closes [#152](https://github.com/beauraines/rtm-cli/issues/152)
|
|
18
|
+
|
|
5
19
|
## [1.11.0](https://github.com/beauraines/rtm-cli/compare/v1.10.0...v1.11.0) (2025-12-17)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ The main usage of the program:
|
|
|
98
98
|
setUrl|su [index] [url] Set the URL of a Task
|
|
99
99
|
start [index] [start...] Set the Start Date of a Task
|
|
100
100
|
tags|t Display all tags
|
|
101
|
-
task [indices...]
|
|
101
|
+
task|tasks [indices...] Display the Task details
|
|
102
102
|
uncomp|unc [indices...] Mark one or more Tasks as not complete
|
|
103
103
|
url [options] [indices...] Display the associated URL of a Task
|
|
104
104
|
whoami Display RTM user information
|
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 = [];
|
|
@@ -83,6 +84,7 @@ async function action(args, env) {
|
|
|
83
84
|
|
|
84
85
|
module.exports = {
|
|
85
86
|
command: 'task [indices...]',
|
|
87
|
+
alias: 'tasks',
|
|
86
88
|
options: [],
|
|
87
89
|
description: 'Display the Task details',
|
|
88
90
|
action: action
|
|
@@ -92,7 +94,7 @@ function displayTask(taskDetails) {
|
|
|
92
94
|
debug(taskDetails)
|
|
93
95
|
let index = taskDetails.index;
|
|
94
96
|
// eslint-disable-next-line no-unused-vars
|
|
95
|
-
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;
|
|
96
98
|
|
|
97
99
|
const listName = LIST_MAP.get(list_id) || "Not found";
|
|
98
100
|
|
|
@@ -111,10 +113,15 @@ function displayTask(taskDetails) {
|
|
|
111
113
|
log(`${completed}`)
|
|
112
114
|
log.style(`Is Recurring: `,styles.index)
|
|
113
115
|
log(`${isRecurring}`)
|
|
116
|
+
if (isRecurring && recurrenceRuleRaw) {
|
|
117
|
+
log.style(`Recurrence: `,styles.index)
|
|
118
|
+
log(humanizeRecurrence(recurrenceRuleRaw))
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
log.style(`Is Subtask: `,styles.index)
|
|
115
122
|
log(`${isSubtask}`)
|
|
116
123
|
log.style(`Estimate: `,styles.index)
|
|
117
|
-
log(
|
|
124
|
+
log(humanizeDuration(estimate))
|
|
118
125
|
log.style(`Url: `,styles.index)
|
|
119
126
|
log(`${url}`)
|
|
120
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 };
|