@eeymoo/hum 0.1.14 → 0.1.15
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/package.json +4 -1
- package/src/commands/config.js +17 -5
- package/src/commands/diet.js +16 -22
- package/src/commands/exercise.js +16 -22
- package/src/commands/record.js +19 -21
- package/src/commands/sleep.js +16 -22
- package/src/commands/timeline.js +7 -9
- package/src/commands/weight.js +16 -22
- package/src/lib/output.js +244 -0
- package/src/lib/timezone.js +78 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eeymoo/hum",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Hum CLI - A command line tool for Hum API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"cli-table3": "^0.6.5",
|
|
20
|
+
"@toon-format/toon": "^2.3.0",
|
|
19
21
|
"commander": "^12.1.0",
|
|
20
22
|
"conf": "^13.0.1",
|
|
23
|
+
"dayjs": "^1.11.13",
|
|
21
24
|
"node-fetch": "^3.3.2"
|
|
22
25
|
},
|
|
23
26
|
"scripts": {
|
package/src/commands/config.js
CHANGED
|
@@ -5,19 +5,31 @@ const configCmd = new Command('config')
|
|
|
5
5
|
|
|
6
6
|
configCmd
|
|
7
7
|
.command('set')
|
|
8
|
-
.argument('<key>', 'Config key (api-url)')
|
|
8
|
+
.argument('<key>', 'Config key (api-url, timezone, dateFormat)')
|
|
9
9
|
.argument('<value>', 'Config value')
|
|
10
10
|
.action((key, value) => {
|
|
11
|
-
const
|
|
11
|
+
const keyMap = {
|
|
12
|
+
'api-url': 'apiUrl',
|
|
13
|
+
'timezone': 'timezone',
|
|
14
|
+
'date-format': 'dateFormat',
|
|
15
|
+
'dateFormat': 'dateFormat'
|
|
16
|
+
}
|
|
17
|
+
const configKey = keyMap[key] || key
|
|
12
18
|
config.set(configKey, value)
|
|
13
19
|
console.log(`Set ${key} to ${value}`)
|
|
14
20
|
})
|
|
15
21
|
|
|
16
22
|
configCmd
|
|
17
23
|
.command('get')
|
|
18
|
-
.argument('<key>', 'Config key (api-url)')
|
|
24
|
+
.argument('<key>', 'Config key (api-url, timezone, dateFormat)')
|
|
19
25
|
.action((key) => {
|
|
20
|
-
const
|
|
26
|
+
const keyMap = {
|
|
27
|
+
'api-url': 'apiUrl',
|
|
28
|
+
'timezone': 'timezone',
|
|
29
|
+
'date-format': 'dateFormat',
|
|
30
|
+
'dateFormat': 'dateFormat'
|
|
31
|
+
}
|
|
32
|
+
const configKey = keyMap[key] || key
|
|
21
33
|
const value = config.get(configKey)
|
|
22
34
|
if (value !== undefined) {
|
|
23
35
|
console.log(value)
|
|
@@ -32,7 +44,7 @@ configCmd
|
|
|
32
44
|
const allConfig = config.store
|
|
33
45
|
console.log('Configuration:')
|
|
34
46
|
for (const [key, value] of Object.entries(allConfig)) {
|
|
35
|
-
const displayKey = key === 'apiUrl' ? 'api-url' : key
|
|
47
|
+
const displayKey = key === 'apiUrl' ? 'api-url' : key === 'dateFormat' ? 'date-format' : key
|
|
36
48
|
console.log(` ${displayKey}: ${value}`)
|
|
37
49
|
}
|
|
38
50
|
})
|
package/src/commands/diet.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { request, createFormData } from '../lib/api.js'
|
|
3
|
+
import { appendTimezoneOffset, buildQueryParams } from '../lib/timezone.js'
|
|
4
|
+
import { outputData } from '../lib/output.js'
|
|
3
5
|
|
|
4
6
|
const diet = new Command('diet')
|
|
5
7
|
|
|
@@ -15,7 +17,7 @@ diet
|
|
|
15
17
|
.option('--foods <string>', 'Foods in format: "name:amount,name2:amount2"')
|
|
16
18
|
.option('--water <value>', 'Water (ml)')
|
|
17
19
|
.option('--note <note>', 'Note')
|
|
18
|
-
.option('--date <date>', 'Date (YYYY-MM-DD)')
|
|
20
|
+
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
19
21
|
.option('--file <paths...>', 'File paths to attach')
|
|
20
22
|
.action(async (options) => {
|
|
21
23
|
try {
|
|
@@ -30,7 +32,7 @@ diet
|
|
|
30
32
|
foods: options.foods,
|
|
31
33
|
water: options.water,
|
|
32
34
|
note: options.note,
|
|
33
|
-
date: options.date
|
|
35
|
+
date: appendTimezoneOffset(options.date)
|
|
34
36
|
}, options.file || [])
|
|
35
37
|
|
|
36
38
|
const result = await request('/diets', {
|
|
@@ -51,19 +53,15 @@ diet
|
|
|
51
53
|
.option('--last <period>', 'Last N days/weeks/months/years')
|
|
52
54
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
53
55
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
56
|
+
.option('--page <number>', 'Page number', '1')
|
|
57
|
+
.option('--limit <number>', 'Items per page', '20')
|
|
54
58
|
.option('--include-deleted', 'Include deleted records')
|
|
59
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
55
60
|
.action(async (options) => {
|
|
56
61
|
try {
|
|
57
|
-
const params =
|
|
58
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
59
|
-
if (value) {
|
|
60
|
-
const paramKey = key === 'meal' ? 'mealType' : key === 'includeDeleted' ? 'includeDeleted' : key
|
|
61
|
-
params.append(paramKey, value === true ? 'true' : value)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
|
|
62
|
+
const { params, page } = buildQueryParams(options)
|
|
65
63
|
const result = await request(`/diets?${params.toString()}`)
|
|
66
|
-
|
|
64
|
+
outputData(result, { format: options.format, type: 'diet-list', page })
|
|
67
65
|
} catch (error) {
|
|
68
66
|
console.error('Failed to list diet records:', error.message)
|
|
69
67
|
}
|
|
@@ -74,17 +72,12 @@ diet
|
|
|
74
72
|
.option('--last <period>', 'Last N days/weeks/months/years')
|
|
75
73
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
76
74
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
75
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
77
76
|
.action(async (options) => {
|
|
78
77
|
try {
|
|
79
|
-
const params =
|
|
80
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
81
|
-
if (value) {
|
|
82
|
-
params.append(key, value)
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
|
|
78
|
+
const { params } = buildQueryParams(options)
|
|
86
79
|
const result = await request(`/diets/stats?${params.toString()}`)
|
|
87
|
-
|
|
80
|
+
outputData(result, { format: options.format, type: 'diet-stats' })
|
|
88
81
|
} catch (error) {
|
|
89
82
|
console.error('Failed to get diet stats:', error.message)
|
|
90
83
|
}
|
|
@@ -93,10 +86,11 @@ diet
|
|
|
93
86
|
diet
|
|
94
87
|
.command('get')
|
|
95
88
|
.requiredOption('--id <id>', 'Diet record ID')
|
|
89
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
96
90
|
.action(async (options) => {
|
|
97
91
|
try {
|
|
98
92
|
const result = await request(`/diets/${options.id}`)
|
|
99
|
-
|
|
93
|
+
outputData(result, { format: options.format, type: 'diet-get' })
|
|
100
94
|
} catch (error) {
|
|
101
95
|
console.error('Failed to get diet record:', error.message)
|
|
102
96
|
}
|
|
@@ -115,7 +109,7 @@ diet
|
|
|
115
109
|
.option('--foods <string>', 'Updated foods')
|
|
116
110
|
.option('--water <value>', 'Updated water')
|
|
117
111
|
.option('--note <note>', 'Updated note')
|
|
118
|
-
.option('--date <date>', 'Updated date (YYYY-MM-DD)')
|
|
112
|
+
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
119
113
|
.option('--file <paths...>', 'File paths to attach')
|
|
120
114
|
.option('--replace-attachments', 'Replace existing attachments instead of adding')
|
|
121
115
|
.action(async (options) => {
|
|
@@ -131,7 +125,7 @@ diet
|
|
|
131
125
|
foods: options.foods,
|
|
132
126
|
water: options.water,
|
|
133
127
|
note: options.note,
|
|
134
|
-
date: options.date,
|
|
128
|
+
date: appendTimezoneOffset(options.date),
|
|
135
129
|
replaceAttachments: options.replaceAttachments ? 'true' : undefined
|
|
136
130
|
}, options.file || [])
|
|
137
131
|
|
package/src/commands/exercise.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { request, createFormData } from '../lib/api.js'
|
|
3
|
+
import { appendTimezoneOffset, buildQueryParams } from '../lib/timezone.js'
|
|
4
|
+
import { outputData } from '../lib/output.js'
|
|
3
5
|
|
|
4
6
|
const exercise = new Command('exercise')
|
|
5
7
|
|
|
@@ -14,7 +16,7 @@ exercise
|
|
|
14
16
|
.option('--feeling <value>', 'Feeling 1-10')
|
|
15
17
|
.option('--location <location>', 'Location')
|
|
16
18
|
.option('--note <note>', 'Note')
|
|
17
|
-
.option('--date <date>', 'Date (YYYY-MM-DD)')
|
|
19
|
+
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
18
20
|
.option('--file <paths...>', 'File paths to attach')
|
|
19
21
|
.action(async (options) => {
|
|
20
22
|
try {
|
|
@@ -28,7 +30,7 @@ exercise
|
|
|
28
30
|
feeling: options.feeling,
|
|
29
31
|
location: options.location,
|
|
30
32
|
note: options.note,
|
|
31
|
-
date: options.date
|
|
33
|
+
date: appendTimezoneOffset(options.date)
|
|
32
34
|
}, options.file || [])
|
|
33
35
|
|
|
34
36
|
const result = await request('/exercises', {
|
|
@@ -49,19 +51,15 @@ exercise
|
|
|
49
51
|
.option('--last <period>', 'Last N days/weeks/months/years')
|
|
50
52
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
51
53
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
54
|
+
.option('--page <number>', 'Page number', '1')
|
|
55
|
+
.option('--limit <number>', 'Items per page', '20')
|
|
52
56
|
.option('--include-deleted', 'Include deleted records')
|
|
57
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
53
58
|
.action(async (options) => {
|
|
54
59
|
try {
|
|
55
|
-
const params =
|
|
56
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
57
|
-
if (value) {
|
|
58
|
-
const paramKey = key === 'includeDeleted' ? 'includeDeleted' : key
|
|
59
|
-
params.append(paramKey, value === true ? 'true' : value)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
60
|
+
const { params, page } = buildQueryParams(options)
|
|
63
61
|
const result = await request(`/exercises?${params.toString()}`)
|
|
64
|
-
|
|
62
|
+
outputData(result, { format: options.format, type: 'exercise-list', page })
|
|
65
63
|
} catch (error) {
|
|
66
64
|
console.error('Failed to list exercise records:', error.message)
|
|
67
65
|
}
|
|
@@ -72,17 +70,12 @@ exercise
|
|
|
72
70
|
.option('--last <period>', 'Last N days/weeks/months/years')
|
|
73
71
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
74
72
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
73
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
75
74
|
.action(async (options) => {
|
|
76
75
|
try {
|
|
77
|
-
const params =
|
|
78
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
79
|
-
if (value) {
|
|
80
|
-
params.append(key, value)
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
|
|
76
|
+
const { params } = buildQueryParams(options)
|
|
84
77
|
const result = await request(`/exercises/stats?${params.toString()}`)
|
|
85
|
-
|
|
78
|
+
outputData(result, { format: options.format, type: 'exercise-stats' })
|
|
86
79
|
} catch (error) {
|
|
87
80
|
console.error('Failed to get exercise stats:', error.message)
|
|
88
81
|
}
|
|
@@ -91,10 +84,11 @@ exercise
|
|
|
91
84
|
exercise
|
|
92
85
|
.command('get')
|
|
93
86
|
.requiredOption('--id <id>', 'Exercise record ID')
|
|
87
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
94
88
|
.action(async (options) => {
|
|
95
89
|
try {
|
|
96
90
|
const result = await request(`/exercises/${options.id}`)
|
|
97
|
-
|
|
91
|
+
outputData(result, { format: options.format, type: 'exercise-get' })
|
|
98
92
|
} catch (error) {
|
|
99
93
|
console.error('Failed to get exercise record:', error.message)
|
|
100
94
|
}
|
|
@@ -112,7 +106,7 @@ exercise
|
|
|
112
106
|
.option('--feeling <value>', 'Updated feeling')
|
|
113
107
|
.option('--location <location>', 'Updated location')
|
|
114
108
|
.option('--note <note>', 'Updated note')
|
|
115
|
-
.option('--date <date>', 'Updated date (YYYY-MM-DD)')
|
|
109
|
+
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
116
110
|
.option('--file <paths...>', 'File paths to attach')
|
|
117
111
|
.option('--replace-attachments', 'Replace existing attachments instead of adding')
|
|
118
112
|
.action(async (options) => {
|
|
@@ -127,7 +121,7 @@ exercise
|
|
|
127
121
|
feeling: options.feeling,
|
|
128
122
|
location: options.location,
|
|
129
123
|
note: options.note,
|
|
130
|
-
date: options.date,
|
|
124
|
+
date: appendTimezoneOffset(options.date),
|
|
131
125
|
replaceAttachments: options.replaceAttachments ? 'true' : undefined
|
|
132
126
|
}, options.file || [])
|
|
133
127
|
|
package/src/commands/record.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { request } from '../lib/api.js'
|
|
3
|
+
import { appendTimezoneOffset, buildQueryParams } from '../lib/timezone.js'
|
|
4
|
+
import { outputData } from '../lib/output.js'
|
|
3
5
|
|
|
4
6
|
const record = new Command('record')
|
|
5
7
|
|
|
@@ -10,7 +12,7 @@ record
|
|
|
10
12
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
11
13
|
.option('--note <note>', 'Note for the record')
|
|
12
14
|
.option('--attachments <urls>', 'Comma-separated attachment URLs')
|
|
13
|
-
.option('--date <date>', 'Date
|
|
15
|
+
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
14
16
|
.action(async (options) => {
|
|
15
17
|
try {
|
|
16
18
|
const recordData = {
|
|
@@ -19,7 +21,7 @@ record
|
|
|
19
21
|
tags: options.tags ? options.tags.split(',') : undefined,
|
|
20
22
|
note: options.note,
|
|
21
23
|
attachments: options.attachments ? options.attachments.split(',') : undefined,
|
|
22
|
-
date: options.date
|
|
24
|
+
date: appendTimezoneOffset(options.date)
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const result = await request('/records', {
|
|
@@ -41,19 +43,15 @@ record
|
|
|
41
43
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
42
44
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
43
45
|
.option('--date <date>', 'Specific date (YYYY-MM-DD)')
|
|
46
|
+
.option('--page <number>', 'Page number', '1')
|
|
47
|
+
.option('--limit <number>', 'Items per page', '20')
|
|
44
48
|
.option('--include-deleted', 'Include deleted records')
|
|
49
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
45
50
|
.action(async (options) => {
|
|
46
51
|
try {
|
|
47
|
-
const params =
|
|
48
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
49
|
-
if (value) {
|
|
50
|
-
const paramKey = key === 'includeDeleted' ? 'includeDeleted' : key
|
|
51
|
-
params.append(paramKey, value === true ? 'true' : value)
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
|
|
52
|
+
const { params, page } = buildQueryParams(options)
|
|
55
53
|
const result = await request(`/records?${params.toString()}`)
|
|
56
|
-
|
|
54
|
+
outputData(result, { format: options.format, type: 'record-list', page })
|
|
57
55
|
} catch (error) {
|
|
58
56
|
console.error('Failed to list records:', error.message)
|
|
59
57
|
}
|
|
@@ -63,6 +61,7 @@ record
|
|
|
63
61
|
.command('get')
|
|
64
62
|
.requiredOption('--id <id>', 'Record ID')
|
|
65
63
|
.option('--include-deleted', 'Include deleted records')
|
|
64
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
66
65
|
.action(async (options) => {
|
|
67
66
|
try {
|
|
68
67
|
const params = new URLSearchParams()
|
|
@@ -71,7 +70,7 @@ record
|
|
|
71
70
|
}
|
|
72
71
|
const queryString = params.toString()
|
|
73
72
|
const result = await request(`/records/${options.id}${queryString ? '?' + queryString : ''}`)
|
|
74
|
-
|
|
73
|
+
outputData(result, { format: options.format, type: 'record-get' })
|
|
75
74
|
} catch (error) {
|
|
76
75
|
console.error('Failed to get record:', error.message)
|
|
77
76
|
}
|
|
@@ -84,7 +83,7 @@ record
|
|
|
84
83
|
.option('--tags <tags>', 'Updated comma-separated tags')
|
|
85
84
|
.option('--note <note>', 'Updated note')
|
|
86
85
|
.option('--attachments <urls>', 'Updated comma-separated attachment URLs')
|
|
87
|
-
.option('--date <date>', 'Updated date (YYYY-MM-DD)')
|
|
86
|
+
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
88
87
|
.action(async (options) => {
|
|
89
88
|
try {
|
|
90
89
|
const updateData = {}
|
|
@@ -92,7 +91,7 @@ record
|
|
|
92
91
|
if (options.tags) updateData.tags = options.tags.split(',')
|
|
93
92
|
if (options.note) updateData.note = options.note
|
|
94
93
|
if (options.attachments) updateData.attachments = options.attachments.split(',')
|
|
95
|
-
if (options.date) updateData.date = options.date
|
|
94
|
+
if (options.date) updateData.date = appendTimezoneOffset(options.date)
|
|
96
95
|
|
|
97
96
|
const result = await request(`/records/${options.id}`, {
|
|
98
97
|
method: 'PATCH',
|
|
@@ -125,17 +124,16 @@ record
|
|
|
125
124
|
.requiredOption('--query <text>', 'Search query')
|
|
126
125
|
.option('--type <type>', 'Filter by type')
|
|
127
126
|
.option('--last <period>', 'Last N days/weeks/months/years')
|
|
127
|
+
.option('--page <number>', 'Page number', '1')
|
|
128
|
+
.option('--limit <number>', 'Items per page', '20')
|
|
128
129
|
.option('--include-deleted', 'Include deleted records')
|
|
130
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
129
131
|
.action(async (options) => {
|
|
130
132
|
try {
|
|
131
|
-
const params =
|
|
132
|
-
params.
|
|
133
|
-
if (options.type) params.append('type', options.type)
|
|
134
|
-
if (options.last) params.append('last', options.last)
|
|
135
|
-
if (options.includeDeleted) params.append('includeDeleted', 'true')
|
|
136
|
-
|
|
133
|
+
const { params, page } = buildQueryParams(options)
|
|
134
|
+
params.set('q', options.query)
|
|
137
135
|
const result = await request(`/records/search?${params.toString()}`)
|
|
138
|
-
|
|
136
|
+
outputData(result, { format: options.format, type: 'record-list', page })
|
|
139
137
|
} catch (error) {
|
|
140
138
|
console.error('Failed to search records:', error.message)
|
|
141
139
|
}
|
package/src/commands/sleep.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { request, createFormData } from '../lib/api.js'
|
|
3
|
+
import { appendTimezoneOffset, buildQueryParams } from '../lib/timezone.js'
|
|
4
|
+
import { outputData } from '../lib/output.js'
|
|
3
5
|
|
|
4
6
|
const sleep = new Command('sleep')
|
|
5
7
|
|
|
@@ -14,7 +16,7 @@ sleep
|
|
|
14
16
|
.option('--awakenings <value>', 'Number of awakenings')
|
|
15
17
|
.option('--feeling <value>', 'Feeling 1-10')
|
|
16
18
|
.option('--note <note>', 'Note')
|
|
17
|
-
.option('--date <date>', 'Date (YYYY-MM-DD)')
|
|
19
|
+
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
18
20
|
.option('--file <paths...>', 'File paths to attach')
|
|
19
21
|
.action(async (options) => {
|
|
20
22
|
try {
|
|
@@ -28,7 +30,7 @@ sleep
|
|
|
28
30
|
awakenings: options.awakenings,
|
|
29
31
|
feeling: options.feeling,
|
|
30
32
|
note: options.note,
|
|
31
|
-
date: options.date
|
|
33
|
+
date: appendTimezoneOffset(options.date)
|
|
32
34
|
}, options.file || [])
|
|
33
35
|
|
|
34
36
|
const result = await request('/sleeps', {
|
|
@@ -48,19 +50,15 @@ sleep
|
|
|
48
50
|
.option('--last <period>', 'Last N days/weeks/months/years')
|
|
49
51
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
50
52
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
53
|
+
.option('--page <number>', 'Page number', '1')
|
|
54
|
+
.option('--limit <number>', 'Items per page', '20')
|
|
51
55
|
.option('--include-deleted', 'Include deleted records')
|
|
56
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
52
57
|
.action(async (options) => {
|
|
53
58
|
try {
|
|
54
|
-
const params =
|
|
55
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
56
|
-
if (value) {
|
|
57
|
-
const paramKey = key === 'includeDeleted' ? 'includeDeleted' : key
|
|
58
|
-
params.append(paramKey, value === true ? 'true' : value)
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
59
|
+
const { params, page } = buildQueryParams(options)
|
|
62
60
|
const result = await request(`/sleeps?${params.toString()}`)
|
|
63
|
-
|
|
61
|
+
outputData(result, { format: options.format, type: 'sleep-list', page })
|
|
64
62
|
} catch (error) {
|
|
65
63
|
console.error('Failed to list sleep records:', error.message)
|
|
66
64
|
}
|
|
@@ -71,17 +69,12 @@ sleep
|
|
|
71
69
|
.option('--last <period>', 'Last N days/weeks/months/years')
|
|
72
70
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
73
71
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
72
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
74
73
|
.action(async (options) => {
|
|
75
74
|
try {
|
|
76
|
-
const params =
|
|
77
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
78
|
-
if (value) {
|
|
79
|
-
params.append(key, value)
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
|
|
75
|
+
const { params } = buildQueryParams(options)
|
|
83
76
|
const result = await request(`/sleeps/stats?${params.toString()}`)
|
|
84
|
-
|
|
77
|
+
outputData(result, { format: options.format, type: 'sleep-stats' })
|
|
85
78
|
} catch (error) {
|
|
86
79
|
console.error('Failed to get sleep stats:', error.message)
|
|
87
80
|
}
|
|
@@ -90,10 +83,11 @@ sleep
|
|
|
90
83
|
sleep
|
|
91
84
|
.command('get')
|
|
92
85
|
.requiredOption('--id <id>', 'Sleep record ID')
|
|
86
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
93
87
|
.action(async (options) => {
|
|
94
88
|
try {
|
|
95
89
|
const result = await request(`/sleeps/${options.id}`)
|
|
96
|
-
|
|
90
|
+
outputData(result, { format: options.format, type: 'sleep-get' })
|
|
97
91
|
} catch (error) {
|
|
98
92
|
console.error('Failed to get sleep record:', error.message)
|
|
99
93
|
}
|
|
@@ -111,7 +105,7 @@ sleep
|
|
|
111
105
|
.option('--awakenings <value>', 'Updated number of awakenings')
|
|
112
106
|
.option('--feeling <value>', 'Updated feeling')
|
|
113
107
|
.option('--note <note>', 'Updated note')
|
|
114
|
-
.option('--date <date>', 'Updated date (YYYY-MM-DD)')
|
|
108
|
+
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
115
109
|
.option('--file <paths...>', 'File paths to attach')
|
|
116
110
|
.option('--replace-attachments', 'Replace existing attachments instead of adding')
|
|
117
111
|
.action(async (options) => {
|
|
@@ -126,7 +120,7 @@ sleep
|
|
|
126
120
|
awakenings: options.awakenings,
|
|
127
121
|
feeling: options.feeling,
|
|
128
122
|
note: options.note,
|
|
129
|
-
date: options.date,
|
|
123
|
+
date: appendTimezoneOffset(options.date),
|
|
130
124
|
replaceAttachments: options.replaceAttachments ? 'true' : undefined
|
|
131
125
|
}, options.file || [])
|
|
132
126
|
|
package/src/commands/timeline.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { request } from '../lib/api.js'
|
|
3
|
+
import { buildQueryParams } from '../lib/timezone.js'
|
|
4
|
+
import { outputData } from '../lib/output.js'
|
|
3
5
|
|
|
4
6
|
const timeline = new Command('timeline')
|
|
5
7
|
|
|
@@ -7,19 +9,15 @@ timeline
|
|
|
7
9
|
.option('--last <period>', 'Last N days/weeks/months/years (e.g., 7d, 2w, 1m)')
|
|
8
10
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
9
11
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
12
|
+
.option('--page <number>', 'Page number', '1')
|
|
13
|
+
.option('--limit <number>', 'Items per page', '20')
|
|
10
14
|
.option('--include-deleted', 'Include deleted records')
|
|
15
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
11
16
|
.action(async (options) => {
|
|
12
17
|
try {
|
|
13
|
-
const params =
|
|
14
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
15
|
-
if (value) {
|
|
16
|
-
const paramKey = key === 'includeDeleted' ? 'includeDeleted' : key
|
|
17
|
-
params.append(paramKey, value === true ? 'true' : value)
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
|
|
18
|
+
const { params, page } = buildQueryParams(options)
|
|
21
19
|
const result = await request(`/timeline?${params.toString()}`)
|
|
22
|
-
|
|
20
|
+
outputData(result, { format: options.format, type: 'timeline', page })
|
|
23
21
|
} catch (error) {
|
|
24
22
|
console.error('Failed to get timeline:', error.message)
|
|
25
23
|
}
|
package/src/commands/weight.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { request, createFormData } from '../lib/api.js'
|
|
3
|
+
import { appendTimezoneOffset, buildQueryParams } from '../lib/timezone.js'
|
|
4
|
+
import { outputData } from '../lib/output.js'
|
|
3
5
|
|
|
4
6
|
const weight = new Command('weight')
|
|
5
7
|
|
|
@@ -13,7 +15,7 @@ weight
|
|
|
13
15
|
.option('--bone-mass <value>', 'Bone mass (kg)')
|
|
14
16
|
.option('--visceral-fat <value>', 'Visceral fat level')
|
|
15
17
|
.option('--note <note>', 'Note for the record')
|
|
16
|
-
.option('--date <date>', 'Date
|
|
18
|
+
.option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
17
19
|
.option('--file <paths...>', 'File paths to attach')
|
|
18
20
|
.action(async (options) => {
|
|
19
21
|
try {
|
|
@@ -26,7 +28,7 @@ weight
|
|
|
26
28
|
boneMass: options.boneMass,
|
|
27
29
|
visceralFat: options.visceralFat,
|
|
28
30
|
note: options.note,
|
|
29
|
-
date: options.date
|
|
31
|
+
date: appendTimezoneOffset(options.date)
|
|
30
32
|
}, options.file || [])
|
|
31
33
|
|
|
32
34
|
const result = await request('/weights', {
|
|
@@ -46,19 +48,15 @@ weight
|
|
|
46
48
|
.option('--last <period>', 'Last N days/weeks/months/years (e.g., 10, 7d, 2w, 6m, 1y)')
|
|
47
49
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
48
50
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
51
|
+
.option('--page <number>', 'Page number', '1')
|
|
52
|
+
.option('--limit <number>', 'Items per page', '20')
|
|
49
53
|
.option('--include-deleted', 'Include deleted records')
|
|
54
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
50
55
|
.action(async (options) => {
|
|
51
56
|
try {
|
|
52
|
-
const params =
|
|
53
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
54
|
-
if (value) {
|
|
55
|
-
const paramKey = key === 'includeDeleted' ? 'includeDeleted' : key
|
|
56
|
-
params.append(paramKey, value === true ? 'true' : value)
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
57
|
+
const { params, page } = buildQueryParams(options)
|
|
60
58
|
const result = await request(`/weights?${params.toString()}`)
|
|
61
|
-
|
|
59
|
+
outputData(result, { format: options.format, type: 'weight-list', page })
|
|
62
60
|
} catch (error) {
|
|
63
61
|
console.error('Failed to list weight records:', error.message)
|
|
64
62
|
}
|
|
@@ -69,17 +67,12 @@ weight
|
|
|
69
67
|
.option('--last <period>', 'Last N days/weeks/months/years (e.g., 10, 7d, 2w, 6m, 1y)')
|
|
70
68
|
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
71
69
|
.option('--end <date>', 'End date (YYYY-MM-DD)')
|
|
70
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
72
71
|
.action(async (options) => {
|
|
73
72
|
try {
|
|
74
|
-
const params =
|
|
75
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
76
|
-
if (value) {
|
|
77
|
-
params.append(key, value)
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
|
|
73
|
+
const { params } = buildQueryParams(options)
|
|
81
74
|
const result = await request(`/weights/stats?${params.toString()}`)
|
|
82
|
-
|
|
75
|
+
outputData(result, { format: options.format, type: 'weight-stats' })
|
|
83
76
|
} catch (error) {
|
|
84
77
|
console.error('Failed to get weight stats:', error.message)
|
|
85
78
|
}
|
|
@@ -88,10 +81,11 @@ weight
|
|
|
88
81
|
weight
|
|
89
82
|
.command('get')
|
|
90
83
|
.requiredOption('--id <id>', 'Weight record ID')
|
|
84
|
+
.option('--format <format>', 'Output format: json, table, toon', 'json')
|
|
91
85
|
.action(async (options) => {
|
|
92
86
|
try {
|
|
93
87
|
const result = await request(`/weights/${options.id}`)
|
|
94
|
-
|
|
88
|
+
outputData(result, { format: options.format, type: 'weight-get' })
|
|
95
89
|
} catch (error) {
|
|
96
90
|
console.error('Failed to get weight record:', error.message)
|
|
97
91
|
}
|
|
@@ -108,7 +102,7 @@ weight
|
|
|
108
102
|
.option('--bone-mass <value>', 'Updated bone mass (kg)')
|
|
109
103
|
.option('--visceral-fat <value>', 'Updated visceral fat level')
|
|
110
104
|
.option('--note <note>', 'Updated note')
|
|
111
|
-
.option('--date <date>', 'Updated date (YYYY-MM-DD)')
|
|
105
|
+
.option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
|
|
112
106
|
.option('--file <paths...>', 'File paths to attach')
|
|
113
107
|
.option('--replace-attachments', 'Replace existing attachments instead of adding')
|
|
114
108
|
.action(async (options) => {
|
|
@@ -122,7 +116,7 @@ weight
|
|
|
122
116
|
boneMass: options.boneMass,
|
|
123
117
|
visceralFat: options.visceralFat,
|
|
124
118
|
note: options.note,
|
|
125
|
-
date: options.date,
|
|
119
|
+
date: appendTimezoneOffset(options.date),
|
|
126
120
|
replaceAttachments: options.replaceAttachments ? 'true' : undefined
|
|
127
121
|
}, options.file || [])
|
|
128
122
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import Table from 'cli-table3'
|
|
2
|
+
import { encode } from '@toon-format/toon'
|
|
3
|
+
import { formatDate, getConfigDateFormat } from './timezone.js'
|
|
4
|
+
|
|
5
|
+
function outputJson(data) {
|
|
6
|
+
console.log(JSON.stringify(data, null, 2))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function outputTable(headers, rows) {
|
|
10
|
+
const table = new Table({
|
|
11
|
+
head: headers,
|
|
12
|
+
style: { head: ['cyan'], border: ['gray'] }
|
|
13
|
+
})
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
table.push(row)
|
|
16
|
+
}
|
|
17
|
+
console.log(table.toString())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function outputToon(data) {
|
|
21
|
+
if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
|
|
22
|
+
console.log('(无数据)')
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
console.log(encode(data))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatPaginationHint(page, totalPages, total, format) {
|
|
29
|
+
if (format !== 'table') return null
|
|
30
|
+
if (totalPages <= 1) return null
|
|
31
|
+
const nextPage = page + 1
|
|
32
|
+
return `第 ${page} 页,共 ${totalPages} 页(共 ${total} 条)— 使用 --page ${nextPage} 查看下一页`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractItems(data, type) {
|
|
36
|
+
const keyMap = {
|
|
37
|
+
'weight-list': 'weights',
|
|
38
|
+
'exercise-list': 'exercises',
|
|
39
|
+
'diet-list': 'diets',
|
|
40
|
+
'sleep-list': 'sleeps',
|
|
41
|
+
'record-list': 'records',
|
|
42
|
+
'timeline': 'items'
|
|
43
|
+
}
|
|
44
|
+
const key = keyMap[type]
|
|
45
|
+
if (key && Array.isArray(data[key])) return data[key]
|
|
46
|
+
if (Array.isArray(data)) return data
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildRows(items, type) {
|
|
51
|
+
const df = getConfigDateFormat()
|
|
52
|
+
switch (type) {
|
|
53
|
+
case 'weight-list':
|
|
54
|
+
return items.map((item, idx) => [
|
|
55
|
+
idx + 1,
|
|
56
|
+
item.id,
|
|
57
|
+
item.weight,
|
|
58
|
+
item.bodyFat ?? '-',
|
|
59
|
+
item.bmi ?? '-',
|
|
60
|
+
item.date ? formatDate(item.date, df) : '-',
|
|
61
|
+
item.note || '-'
|
|
62
|
+
])
|
|
63
|
+
case 'exercise-list':
|
|
64
|
+
return items.map((item, idx) => [
|
|
65
|
+
idx + 1,
|
|
66
|
+
item.id,
|
|
67
|
+
item.type,
|
|
68
|
+
item.duration,
|
|
69
|
+
item.caloriesBurned ?? '-',
|
|
70
|
+
item.heartRateAvg ?? '-',
|
|
71
|
+
item.date ? formatDate(item.date, df) : '-'
|
|
72
|
+
])
|
|
73
|
+
case 'diet-list':
|
|
74
|
+
return items.map((item, idx) => [
|
|
75
|
+
idx + 1,
|
|
76
|
+
item.id,
|
|
77
|
+
item.mealType,
|
|
78
|
+
item.calories ?? '-',
|
|
79
|
+
item.protein ?? '-',
|
|
80
|
+
item.carbs ?? '-',
|
|
81
|
+
item.fat ?? '-',
|
|
82
|
+
item.date ? formatDate(item.date, df) : '-'
|
|
83
|
+
])
|
|
84
|
+
case 'sleep-list':
|
|
85
|
+
return items.map((item, idx) => [
|
|
86
|
+
idx + 1,
|
|
87
|
+
item.id,
|
|
88
|
+
item.duration,
|
|
89
|
+
item.quality,
|
|
90
|
+
item.bedTime ?? '-',
|
|
91
|
+
item.wakeTime ?? '-',
|
|
92
|
+
item.deepSleep ?? '-',
|
|
93
|
+
item.date ? formatDate(item.date, df) : '-'
|
|
94
|
+
])
|
|
95
|
+
case 'record-list':
|
|
96
|
+
return items.map((item, idx) => [
|
|
97
|
+
idx + 1,
|
|
98
|
+
item.id,
|
|
99
|
+
item.type,
|
|
100
|
+
(item.tags || []).join(', ') || '-',
|
|
101
|
+
item.date ? formatDate(item.date, df) : '-',
|
|
102
|
+
item.note || '-'
|
|
103
|
+
])
|
|
104
|
+
case 'timeline':
|
|
105
|
+
return items.map((item, idx) => {
|
|
106
|
+
let summary = ''
|
|
107
|
+
if (item.type === 'weight') summary = `${item.data?.weight} kg`
|
|
108
|
+
else if (item.type === 'exercise') summary = `${item.data?.duration} min ${item.data?.type || ''}`
|
|
109
|
+
else if (item.type === 'diet') summary = `${item.data?.calories || 0} kcal (${item.data?.mealType || ''})`
|
|
110
|
+
else if (item.type === 'sleep') summary = `${item.data?.duration}h, 质量: ${item.data?.quality}/10`
|
|
111
|
+
else if (item.type === 'record') summary = item.data?.type || ''
|
|
112
|
+
return [
|
|
113
|
+
idx + 1,
|
|
114
|
+
item.date ? formatDate(item.date, df) : '-',
|
|
115
|
+
item.type,
|
|
116
|
+
summary
|
|
117
|
+
]
|
|
118
|
+
})
|
|
119
|
+
default:
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getHeaders(type) {
|
|
125
|
+
switch (type) {
|
|
126
|
+
case 'weight-list': return ['#', 'ID', '体重(kg)', '体脂(%)', 'BMI', '日期', '备注']
|
|
127
|
+
case 'exercise-list': return ['#', 'ID', '类型', '时长(分)', '热量', '心率', '日期']
|
|
128
|
+
case 'diet-list': return ['#', 'ID', '餐别', '热量', '蛋白质', '碳水', '脂肪', '日期']
|
|
129
|
+
case 'sleep-list': return ['#', 'ID', '时长(h)', '质量', '入睡', '醒来', '深睡(h)', '日期']
|
|
130
|
+
case 'record-list': return ['#', 'ID', '类型', '标签', '日期', '备注']
|
|
131
|
+
case 'timeline': return ['#', '时间', '类型', '摘要']
|
|
132
|
+
default: return []
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function outputStats(data, type, format) {
|
|
137
|
+
if (format === 'json') {
|
|
138
|
+
outputJson(data)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (format === 'toon') {
|
|
142
|
+
outputToon(data)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rows = []
|
|
147
|
+
switch (type) {
|
|
148
|
+
case 'weight-stats':
|
|
149
|
+
if (data.avgWeight !== undefined) rows.push(['平均体重', `${data.avgWeight} kg`])
|
|
150
|
+
if (data.minWeight !== undefined) rows.push(['最低体重', `${data.minWeight} kg`])
|
|
151
|
+
if (data.maxWeight !== undefined) rows.push(['最高体重', `${data.maxWeight} kg`])
|
|
152
|
+
if (data.change !== undefined && data.change !== null) rows.push(['变化量', `${data.change > 0 ? '+' : ''}${data.change} kg`])
|
|
153
|
+
break
|
|
154
|
+
case 'exercise-stats':
|
|
155
|
+
if (data.count !== undefined) rows.push(['总次数', data.count])
|
|
156
|
+
if (data.totalDuration !== undefined) rows.push(['总时长', `${data.totalDuration} min`])
|
|
157
|
+
if (data.totalCalories !== undefined) rows.push(['总热量', `${data.totalCalories} kcal`])
|
|
158
|
+
if (data.frequencyByType) {
|
|
159
|
+
for (const [t, c] of Object.entries(data.frequencyByType)) {
|
|
160
|
+
rows.push([`频率 (${t})`, c])
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
break
|
|
164
|
+
case 'diet-stats':
|
|
165
|
+
if (data.avgCaloriesPerDay !== undefined) rows.push(['日均热量', `${data.avgCaloriesPerDay?.toFixed?.(0) || data.avgCaloriesPerDay} kcal`])
|
|
166
|
+
if (data.avgProtein !== undefined) rows.push(['平均蛋白质', `${data.avgProtein?.toFixed?.(1) || data.avgProtein}g`])
|
|
167
|
+
if (data.avgCarbs !== undefined) rows.push(['平均碳水', `${data.avgCarbs?.toFixed?.(1) || data.avgCarbs}g`])
|
|
168
|
+
if (data.avgFat !== undefined) rows.push(['平均脂肪', `${data.avgFat?.toFixed?.(1) || data.avgFat}g`])
|
|
169
|
+
if (data.totalWater !== undefined && data.totalWater !== null) rows.push(['总饮水量', `${data.totalWater}ml`])
|
|
170
|
+
if (data.count !== undefined) rows.push(['记录数', data.count])
|
|
171
|
+
break
|
|
172
|
+
case 'sleep-stats':
|
|
173
|
+
if (data.avgDuration !== undefined) rows.push(['平均时长', `${data.avgDuration?.toFixed?.(1) || data.avgDuration}h`])
|
|
174
|
+
if (data.avgQuality !== undefined) rows.push(['平均质量', `${data.avgQuality?.toFixed?.(1) || data.avgQuality}/10`])
|
|
175
|
+
if (data.avgDeepSleep !== undefined) rows.push(['平均深睡', `${data.avgDeepSleep?.toFixed?.(1) || data.avgDeepSleep}h`])
|
|
176
|
+
if (data.count !== undefined) rows.push(['记录数', data.count])
|
|
177
|
+
break
|
|
178
|
+
default:
|
|
179
|
+
rows.push(...Object.entries(data).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : v]))
|
|
180
|
+
}
|
|
181
|
+
outputTable(['指标', '数值'], rows)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function outputGet(data, type, format) {
|
|
185
|
+
if (format === 'json') {
|
|
186
|
+
outputJson(data)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if (format === 'toon') {
|
|
190
|
+
outputToon(data)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const df = getConfigDateFormat()
|
|
195
|
+
const rows = Object.entries(data).map(([k, v]) => {
|
|
196
|
+
let val = v
|
|
197
|
+
if (k === 'date' && v) val = formatDate(v, df)
|
|
198
|
+
else if (typeof v === 'object' && v !== null) val = JSON.stringify(v)
|
|
199
|
+
return [k, val]
|
|
200
|
+
})
|
|
201
|
+
outputTable(['字段', '值'], rows)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function outputData(data, options = {}) {
|
|
205
|
+
const { format = 'json', type, page = 1 } = options
|
|
206
|
+
|
|
207
|
+
if (type && type.endsWith('-stats')) {
|
|
208
|
+
outputStats(data, type, format)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (type && type.endsWith('-get')) {
|
|
213
|
+
outputGet(data, type, format)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (format === 'toon') {
|
|
218
|
+
outputToon(data)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const items = extractItems(data, type)
|
|
223
|
+
const headers = getHeaders(type)
|
|
224
|
+
const rows = buildRows(items, type)
|
|
225
|
+
|
|
226
|
+
if (format === 'json') {
|
|
227
|
+
outputJson(data)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (format === 'table') {
|
|
232
|
+
if (rows.length > 0) {
|
|
233
|
+
outputTable(headers, rows)
|
|
234
|
+
} else {
|
|
235
|
+
console.log('(无数据)')
|
|
236
|
+
}
|
|
237
|
+
const hint = formatPaginationHint(page, data.totalPages, data.total, 'table')
|
|
238
|
+
if (hint) console.log('\n' + hint)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// fallback
|
|
243
|
+
outputJson(data)
|
|
244
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import utc from 'dayjs/plugin/utc.js'
|
|
3
|
+
import timezone from 'dayjs/plugin/timezone.js'
|
|
4
|
+
import config from './config.js'
|
|
5
|
+
|
|
6
|
+
dayjs.extend(utc)
|
|
7
|
+
dayjs.extend(timezone)
|
|
8
|
+
|
|
9
|
+
export function getConfigTimezone() {
|
|
10
|
+
return config.get('timezone') || dayjs.tz.guess() || 'UTC'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getConfigDateFormat() {
|
|
14
|
+
return config.get('dateFormat') || 'YYYY-MM-DD HH:mm'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatDate(date, format) {
|
|
18
|
+
const tz = getConfigTimezone()
|
|
19
|
+
const fmt = format || getConfigDateFormat()
|
|
20
|
+
return dayjs(date).tz(tz).format(fmt)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function appendTimezoneOffset(dateStr) {
|
|
24
|
+
if (!dateStr) return dateStr
|
|
25
|
+
// Pure date like "2026-05-28" - no offset needed
|
|
26
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
27
|
+
return dateStr
|
|
28
|
+
}
|
|
29
|
+
// Already has timezone offset
|
|
30
|
+
if (/[+-]\d{2}:\d{2}$/.test(dateStr) || /Z$/.test(dateStr)) {
|
|
31
|
+
return dateStr
|
|
32
|
+
}
|
|
33
|
+
// Datetime without offset - append user's timezone
|
|
34
|
+
const tz = getConfigTimezone()
|
|
35
|
+
const offset = dayjs().tz(tz).format('Z')
|
|
36
|
+
return `${dateStr}${offset}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getDefaultLast() {
|
|
40
|
+
return '7d'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getDefaultLimit() {
|
|
44
|
+
return 20
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildQueryParams(options, defaults = {}) {
|
|
48
|
+
const params = new URLSearchParams()
|
|
49
|
+
|
|
50
|
+
const hasTimeFilter = options.last || options.start || options.end
|
|
51
|
+
if (!hasTimeFilter && defaults.last !== false) {
|
|
52
|
+
params.append('last', getDefaultLast())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options.last) params.append('last', options.last)
|
|
56
|
+
if (options.start) params.append('start', options.start)
|
|
57
|
+
if (options.end) params.append('end', options.end)
|
|
58
|
+
|
|
59
|
+
const page = options.page || defaults.page || 1
|
|
60
|
+
const limit = options.limit || defaults.limit || getDefaultLimit()
|
|
61
|
+
params.append('page', String(page))
|
|
62
|
+
params.append('limit', String(limit))
|
|
63
|
+
|
|
64
|
+
if (options.includeDeleted) {
|
|
65
|
+
params.append('includeDeleted', 'true')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Pass through other known filter options
|
|
69
|
+
const filterKeys = ['type', 'tag', 'meal', 'query']
|
|
70
|
+
for (const key of filterKeys) {
|
|
71
|
+
if (options[key]) {
|
|
72
|
+
const paramKey = key === 'meal' ? 'mealType' : key === 'query' ? 'q' : key
|
|
73
|
+
params.append(paramKey, options[key])
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { params, page: parseInt(String(page), 10), limit: parseInt(String(limit), 10) }
|
|
78
|
+
}
|