@eeymoo/hum 0.1.23 → 0.1.25

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eeymoo/hum",
3
- "version": "0.1.23",
4
- "description": "Hum CLI - A command line tool for Hum API",
3
+ "version": "0.1.25",
4
+ "description": "Hum CLI - A command line tool for Hum API",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Eeymoo/hum.git",
@@ -20,12 +20,12 @@
20
20
  "@toon-format/toon": "^2.3.0",
21
21
  "commander": "^12.1.0",
22
22
  "conf": "^13.0.1",
23
- "dayjs": "^1.11.13",
24
- "node-fetch": "^3.3.2"
23
+ "dayjs": "^1.11.13"
25
24
  },
26
25
  "scripts": {
27
26
  "build": "node bin/index.js --version",
28
- "test:e2e": "bash test/e2e.sh"
27
+ "test:e2e": "bash test/e2e.sh",
28
+ "test:e2e:extended": "bash test/e2e-extended.sh"
29
29
  },
30
30
  "devDependencies": {
31
31
  "typescript": "^5.6.0"
@@ -16,7 +16,7 @@ auth
16
16
  console.log('Please visit:', deviceData.verificationUriComplete)
17
17
  console.log('Code:', deviceData.userCode)
18
18
  console.log('Waiting for authorization...')
19
-
19
+
20
20
  const maxAttempts = 60
21
21
  for (let i = 0; i < maxAttempts; i++) {
22
22
  await new Promise(resolve => setTimeout(resolve, deviceData.interval * 1000))
@@ -28,10 +28,10 @@ auth
28
28
  grantType: 'urn:ietf:params:oauth:grant-type:device_code'
29
29
  })
30
30
  })
31
-
32
- if (tokenData.accessToken) {
33
- config.set('accessToken', tokenData.accessToken)
34
- config.set('refreshToken', tokenData.refreshToken)
31
+
32
+ if (tokenData.access_token) {
33
+ config.set('accessToken', tokenData.access_token)
34
+ config.set('refreshToken', tokenData.refresh_token)
35
35
  console.log('Successfully logged in!')
36
36
  return
37
37
  }
@@ -40,14 +40,21 @@ auth
40
40
  }
41
41
  }
42
42
  console.error('Authorization timeout')
43
- } else if (options.apiKey) {
43
+ } else if (options.apiKey || process.env.HUM_API_KEY) {
44
+ const apiKey = options.apiKey || process.env.HUM_API_KEY
44
45
  const result = await request('/auth/verify', {
45
46
  method: 'POST',
46
- body: JSON.stringify({ apiKey: options.apiKey })
47
+ body: JSON.stringify({ apiKey })
48
+ }).catch(err => {
49
+ // verify 端点在 key 无效时返回 401,捕获后返回统一结构
50
+ if (err.message.includes('401')) {
51
+ return { valid: false }
52
+ }
53
+ throw err
47
54
  })
48
55
 
49
56
  if (result.valid) {
50
- config.set('apiKey', options.apiKey)
57
+ config.set('apiKey', apiKey)
51
58
  const parts = []
52
59
  if (result.user) parts.push(result.user)
53
60
  if (result.keyName) parts.push(`key: ${result.keyName}`)
@@ -56,7 +63,7 @@ auth
56
63
  console.error('Invalid API key')
57
64
  }
58
65
  } else {
59
- console.error('Please provide --api-key or --device option')
66
+ console.error('Please provide --api-key or --device option, or set HUM_API_KEY environment variable')
60
67
  }
61
68
  } catch (error) {
62
69
  console.error('Login failed:', error.message)
@@ -45,7 +45,12 @@ configCmd
45
45
  console.log('Configuration:')
46
46
  for (const [key, value] of Object.entries(allConfig)) {
47
47
  const displayKey = key === 'apiUrl' ? 'api-url' : key === 'dateFormat' ? 'date-format' : key
48
- console.log(` ${displayKey}: ${value}`)
48
+ let displayValue = value
49
+ // 脱敏敏感信息
50
+ if ((key === 'apiKey' || key === 'accessToken' || key === 'refreshToken') && typeof value === 'string' && value.length > 8) {
51
+ displayValue = value.slice(0, 4) + '****' + value.slice(-4)
52
+ }
53
+ console.log(` ${displayKey}: ${displayValue}`)
49
54
  }
50
55
  })
51
56
 
@@ -1,160 +1,21 @@
1
- import { Command } from 'commander'
2
- import { request, createFormData } from '../lib/api.js'
3
- import { appendTimezoneOffset, buildQueryParams } from '../lib/timezone.js'
4
- import { outputData } from '../lib/output.js'
5
-
6
- const diet = new Command('diet')
7
-
8
- diet
9
- .command('add')
10
- .requiredOption('--meal <type>', 'Meal type (breakfast/lunch/dinner/snack)')
11
- .option('--calories <value>', 'Calories')
12
- .option('--protein <value>', 'Protein (g)')
13
- .option('--carbs <value>', 'Carbs (g)')
14
- .option('--fat <value>', 'Fat (g)')
15
- .option('--fiber <value>', 'Fiber (g)')
16
- .option('--sodium <value>', 'Sodium (mg)')
17
- .option('--foods <string>', 'Foods in format: "name:amount,name2:amount2"')
18
- .option('--water <value>', 'Water (ml)')
19
- .option('--extra-data <json>', 'Extra data (JSON string)')
20
- .option('--note <note>', 'Note')
21
- .option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
22
- .option('--file <paths...>', 'File paths to attach')
23
- .action(async (options) => {
24
- try {
25
- const formData = createFormData({
26
- mealType: options.meal,
27
- calories: options.calories,
28
- protein: options.protein,
29
- carbs: options.carbs,
30
- fat: options.fat,
31
- fiber: options.fiber,
32
- sodium: options.sodium,
33
- foods: options.foods,
34
- water: options.water,
35
- extraData: options.extraData,
36
- note: options.note,
37
- date: appendTimezoneOffset(options.date)
38
- }, options.file || [])
39
-
40
- const result = await request('/diets', {
41
- method: 'POST',
42
- body: formData,
43
- isFormData: true
44
- })
45
-
46
- console.log('Diet record added:', result.id)
47
- } catch (error) {
48
- console.error('Failed to add diet record:', error.message)
49
- }
50
- })
51
-
52
- diet
53
- .command('list')
54
- .option('--meal <type>', 'Filter by meal type')
55
- .option('--last <period>', 'Last N days/weeks/months/years')
56
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
57
- .option('--end <date>', 'End date (YYYY-MM-DD)')
58
- .option('--page <number>', 'Page number', '1')
59
- .option('--limit <number>', 'Items per page', '20')
60
- .option('--include-deleted', 'Include deleted records')
61
- .option('--format <format>', 'Output format: json, table, toon', 'json')
62
- .action(async (options) => {
63
- try {
64
- const { params, page } = buildQueryParams(options)
65
- const result = await request(`/diets?${params.toString()}`)
66
- outputData(result, { format: options.format, type: 'diet-list', page })
67
- } catch (error) {
68
- console.error('Failed to list diet records:', error.message)
69
- }
70
- })
71
-
72
- diet
73
- .command('stats')
74
- .option('--last <period>', 'Last N days/weeks/months/years')
75
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
76
- .option('--end <date>', 'End date (YYYY-MM-DD)')
77
- .option('--format <format>', 'Output format: json, table, toon', 'json')
78
- .action(async (options) => {
79
- try {
80
- const { params } = buildQueryParams(options)
81
- const result = await request(`/diets/stats?${params.toString()}`)
82
- outputData(result, { format: options.format, type: 'diet-stats' })
83
- } catch (error) {
84
- console.error('Failed to get diet stats:', error.message)
85
- }
86
- })
87
-
88
- diet
89
- .command('get')
90
- .requiredOption('--id <id>', 'Diet record ID')
91
- .option('--format <format>', 'Output format: json, table, toon', 'json')
92
- .action(async (options) => {
93
- try {
94
- const result = await request(`/diets/${options.id}`)
95
- outputData(result, { format: options.format, type: 'diet-get' })
96
- } catch (error) {
97
- console.error('Failed to get diet record:', error.message)
98
- }
99
- })
100
-
101
- diet
102
- .command('update')
103
- .requiredOption('--id <id>', 'Diet record ID')
104
- .option('--meal <type>', 'Updated meal type')
105
- .option('--calories <value>', 'Updated calories')
106
- .option('--protein <value>', 'Updated protein')
107
- .option('--carbs <value>', 'Updated carbs')
108
- .option('--fat <value>', 'Updated fat')
109
- .option('--fiber <value>', 'Updated fiber')
110
- .option('--sodium <value>', 'Updated sodium')
111
- .option('--foods <string>', 'Updated foods')
112
- .option('--water <value>', 'Updated water')
113
- .option('--extra-data <json>', 'Updated extra data (JSON string)')
114
- .option('--note <note>', 'Updated note')
115
- .option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
116
- .option('--file <paths...>', 'File paths to attach')
117
- .option('--replace-attachments', 'Replace existing attachments instead of adding')
118
- .action(async (options) => {
119
- try {
120
- const formData = createFormData({
121
- mealType: options.meal,
122
- calories: options.calories,
123
- protein: options.protein,
124
- carbs: options.carbs,
125
- fat: options.fat,
126
- fiber: options.fiber,
127
- sodium: options.sodium,
128
- foods: options.foods,
129
- water: options.water,
130
- extraData: options.extraData,
131
- note: options.note,
132
- date: appendTimezoneOffset(options.date),
133
- replaceAttachments: options.replaceAttachments ? 'true' : undefined
134
- }, options.file || [])
135
-
136
- const result = await request(`/diets/${options.id}`, {
137
- method: 'PATCH',
138
- body: formData,
139
- isFormData: true
140
- })
141
-
142
- console.log('Diet record updated:', result.id)
143
- } catch (error) {
144
- console.error('Failed to update diet record:', error.message)
145
- }
146
- })
147
-
148
- diet
149
- .command('delete')
150
- .requiredOption('--id <id>', 'Diet record ID')
151
- .action(async (options) => {
152
- try {
153
- await request(`/diets/${options.id}`, { method: 'DELETE' })
154
- console.log('Diet record deleted')
155
- } catch (error) {
156
- console.error('Failed to delete diet record:', error.message)
157
- }
158
- })
1
+ import { createCrudCommand } from '../lib/crud-command.js'
2
+
3
+ const diet = createCrudCommand('diet', {
4
+ endpoint: '/diets',
5
+ fields: [
6
+ { flag: 'meal', description: 'Meal type (breakfast/lunch/dinner/snack)', formKey: 'mealType', required: true },
7
+ { flag: 'calories', description: 'Calories' },
8
+ { flag: 'protein', description: 'Protein (g)' },
9
+ { flag: 'carbs', description: 'Carbs (g)' },
10
+ { flag: 'fat', description: 'Fat (g)' },
11
+ { flag: 'fiber', description: 'Fiber (g)' },
12
+ { flag: 'sodium', description: 'Sodium (mg)' },
13
+ { flag: 'foods', description: 'Foods in format: "name:amount,name2:amount2"' },
14
+ { flag: 'water', description: 'Water (ml)' },
15
+ { flag: 'extra-data', description: 'Extra data (JSON string)', formKey: 'extraData' },
16
+ { flag: 'note', description: 'Note' }
17
+ ],
18
+ fileFields: ['file']
19
+ })
159
20
 
160
21
  export default diet
@@ -1,156 +1,20 @@
1
- import { Command } from 'commander'
2
- import { request, createFormData } from '../lib/api.js'
3
- import { appendTimezoneOffset, buildQueryParams } from '../lib/timezone.js'
4
- import { outputData } from '../lib/output.js'
5
-
6
- const exercise = new Command('exercise')
7
-
8
- exercise
9
- .command('add')
10
- .requiredOption('--type <type>', 'Exercise type (running/strength/cycling/swimming/other)')
11
- .requiredOption('--duration <value>', 'Duration in minutes')
12
- .option('--calories <value>', 'Calories burned')
13
- .option('--activities <string>', 'Activities in format: "name:prop1=val1,prop2=val2;name2:prop1=val1"')
14
- .option('--heart-rate-avg <value>', 'Average heart rate')
15
- .option('--heart-rate-max <value>', 'Max heart rate')
16
- .option('--feeling <value>', 'Feeling 1-10')
17
- .option('--extra-data <json>', 'Extra data (JSON string)')
18
- .option('--location <location>', 'Location')
19
- .option('--note <note>', 'Note')
20
- .option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
21
- .option('--file <paths...>', 'File paths to attach')
22
- .action(async (options) => {
23
- try {
24
- const formData = createFormData({
25
- type: options.type,
26
- duration: options.duration,
27
- caloriesBurned: options.calories,
28
- activities: options.activities,
29
- heartRateAvg: options.heartRateAvg,
30
- heartRateMax: options.heartRateMax,
31
- feeling: options.feeling,
32
- extraData: options.extraData,
33
- location: options.location,
34
- note: options.note,
35
- date: appendTimezoneOffset(options.date)
36
- }, options.file || [])
37
-
38
- const result = await request('/exercises', {
39
- method: 'POST',
40
- body: formData,
41
- isFormData: true
42
- })
43
-
44
- console.log('Exercise record added:', result.id)
45
- } catch (error) {
46
- console.error('Failed to add exercise record:', error.message)
47
- }
48
- })
49
-
50
- exercise
51
- .command('list')
52
- .option('--type <type>', 'Filter by type')
53
- .option('--last <period>', 'Last N days/weeks/months/years')
54
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
55
- .option('--end <date>', 'End date (YYYY-MM-DD)')
56
- .option('--page <number>', 'Page number', '1')
57
- .option('--limit <number>', 'Items per page', '20')
58
- .option('--include-deleted', 'Include deleted records')
59
- .option('--format <format>', 'Output format: json, table, toon', 'json')
60
- .action(async (options) => {
61
- try {
62
- const { params, page } = buildQueryParams(options)
63
- const result = await request(`/exercises?${params.toString()}`)
64
- outputData(result, { format: options.format, type: 'exercise-list', page })
65
- } catch (error) {
66
- console.error('Failed to list exercise records:', error.message)
67
- }
68
- })
69
-
70
- exercise
71
- .command('stats')
72
- .option('--last <period>', 'Last N days/weeks/months/years')
73
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
74
- .option('--end <date>', 'End date (YYYY-MM-DD)')
75
- .option('--format <format>', 'Output format: json, table, toon', 'json')
76
- .action(async (options) => {
77
- try {
78
- const { params } = buildQueryParams(options)
79
- const result = await request(`/exercises/stats?${params.toString()}`)
80
- outputData(result, { format: options.format, type: 'exercise-stats' })
81
- } catch (error) {
82
- console.error('Failed to get exercise stats:', error.message)
83
- }
84
- })
85
-
86
- exercise
87
- .command('get')
88
- .requiredOption('--id <id>', 'Exercise record ID')
89
- .option('--format <format>', 'Output format: json, table, toon', 'json')
90
- .action(async (options) => {
91
- try {
92
- const result = await request(`/exercises/${options.id}`)
93
- outputData(result, { format: options.format, type: 'exercise-get' })
94
- } catch (error) {
95
- console.error('Failed to get exercise record:', error.message)
96
- }
97
- })
98
-
99
- exercise
100
- .command('update')
101
- .requiredOption('--id <id>', 'Exercise record ID')
102
- .option('--type <type>', 'Updated type')
103
- .option('--duration <value>', 'Updated duration')
104
- .option('--calories <value>', 'Updated calories burned')
105
- .option('--activities <string>', 'Updated activities')
106
- .option('--heart-rate-avg <value>', 'Updated average heart rate')
107
- .option('--heart-rate-max <value>', 'Updated max heart rate')
108
- .option('--feeling <value>', 'Updated feeling')
109
- .option('--extra-data <json>', 'Updated extra data (JSON string)')
110
- .option('--location <location>', 'Updated location')
111
- .option('--note <note>', 'Updated note')
112
- .option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
113
- .option('--file <paths...>', 'File paths to attach')
114
- .option('--replace-attachments', 'Replace existing attachments instead of adding')
115
- .action(async (options) => {
116
- try {
117
- const formData = createFormData({
118
- type: options.type,
119
- duration: options.duration,
120
- caloriesBurned: options.calories,
121
- activities: options.activities,
122
- heartRateAvg: options.heartRateAvg,
123
- heartRateMax: options.heartRateMax,
124
- feeling: options.feeling,
125
- extraData: options.extraData,
126
- location: options.location,
127
- note: options.note,
128
- date: appendTimezoneOffset(options.date),
129
- replaceAttachments: options.replaceAttachments ? 'true' : undefined
130
- }, options.file || [])
131
-
132
- const result = await request(`/exercises/${options.id}`, {
133
- method: 'PATCH',
134
- body: formData,
135
- isFormData: true
136
- })
137
-
138
- console.log('Exercise record updated:', result.id)
139
- } catch (error) {
140
- console.error('Failed to update exercise record:', error.message)
141
- }
142
- })
143
-
144
- exercise
145
- .command('delete')
146
- .requiredOption('--id <id>', 'Exercise record ID')
147
- .action(async (options) => {
148
- try {
149
- await request(`/exercises/${options.id}`, { method: 'DELETE' })
150
- console.log('Exercise record deleted')
151
- } catch (error) {
152
- console.error('Failed to delete exercise record:', error.message)
153
- }
154
- })
1
+ import { createCrudCommand } from '../lib/crud-command.js'
2
+
3
+ const exercise = createCrudCommand('exercise', {
4
+ endpoint: '/exercises',
5
+ fields: [
6
+ { flag: 'type', description: 'Exercise type (running/strength/cycling/swimming/other)', required: true },
7
+ { flag: 'duration', description: 'Duration in minutes', required: true },
8
+ { flag: 'calories', description: 'Calories burned', formKey: 'caloriesBurned' },
9
+ { flag: 'activities', description: 'Activities in format: "name:prop1=val1,prop2=val2;name2:prop1=val1"' },
10
+ { flag: 'heart-rate-avg', description: 'Average heart rate', formKey: 'heartRateAvg' },
11
+ { flag: 'heart-rate-max', description: 'Max heart rate', formKey: 'heartRateMax' },
12
+ { flag: 'feeling', description: 'Feeling 1-10' },
13
+ { flag: 'extra-data', description: 'Extra data (JSON string)', formKey: 'extraData' },
14
+ { flag: 'location', description: 'Location' },
15
+ { flag: 'note', description: 'Note' }
16
+ ],
17
+ fileFields: ['file']
18
+ })
155
19
 
156
20
  export default exercise
@@ -21,7 +21,7 @@ food
21
21
  const cleaned = items.map(({ rawItem, ...rest }) => rest)
22
22
  outputData({ foods: cleaned, totalPages: 1, total: cleaned.length }, { format: options.format, type: 'food-list' })
23
23
  } catch (error) {
24
- console.error(error.message)
24
+ console.error('查询食物失败:', error.message)
25
25
  process.exitCode = 1
26
26
  }
27
27
  })
@@ -29,9 +29,9 @@ record
29
29
  body: JSON.stringify(recordData)
30
30
  })
31
31
 
32
- console.log('Record added:', result.id)
32
+ console.log('记录已添加:', result.id)
33
33
  } catch (error) {
34
- console.error('Failed to add record:', error.message)
34
+ console.error('添加记录失败:', error.message)
35
35
  }
36
36
  })
37
37
 
@@ -53,7 +53,7 @@ record
53
53
  const result = await request(`/records?${params.toString()}`)
54
54
  outputData(result, { format: options.format, type: 'record-list', page })
55
55
  } catch (error) {
56
- console.error('Failed to list records:', error.message)
56
+ console.error('获取记录列表失败:', error.message)
57
57
  }
58
58
  })
59
59
 
@@ -72,7 +72,7 @@ record
72
72
  const result = await request(`/records/${options.id}${queryString ? '?' + queryString : ''}`)
73
73
  outputData(result, { format: options.format, type: 'record-get' })
74
74
  } catch (error) {
75
- console.error('Failed to get record:', error.message)
75
+ console.error('获取记录失败:', error.message)
76
76
  }
77
77
  })
78
78
 
@@ -98,9 +98,9 @@ record
98
98
  body: JSON.stringify(updateData)
99
99
  })
100
100
 
101
- console.log('Record updated:', result.id)
101
+ console.log('记录已更新:', result.id)
102
102
  } catch (error) {
103
- console.error('Failed to update record:', error.message)
103
+ console.error('更新记录失败:', error.message)
104
104
  }
105
105
  })
106
106
 
@@ -113,9 +113,9 @@ record
113
113
  method: 'DELETE'
114
114
  })
115
115
 
116
- console.log('Record deleted')
116
+ console.log('记录已删除')
117
117
  } catch (error) {
118
- console.error('Failed to delete record:', error.message)
118
+ console.error('删除记录失败:', error.message)
119
119
  }
120
120
  })
121
121
 
@@ -135,7 +135,7 @@ record
135
135
  const result = await request(`/records/search?${params.toString()}`)
136
136
  outputData(result, { format: options.format, type: 'record-list', page })
137
137
  } catch (error) {
138
- console.error('Failed to search records:', error.message)
138
+ console.error('搜索记录失败:', error.message)
139
139
  }
140
140
  })
141
141