@eeymoo/hum 0.1.23 → 0.1.24

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeymoo/hum",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Hum CLI - A command line tool for Hum API",
5
5
  "repository": {
6
6
  "type": "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"
@@ -40,14 +40,15 @@ 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 })
47
48
  })
48
49
 
49
50
  if (result.valid) {
50
- config.set('apiKey', options.apiKey)
51
+ config.set('apiKey', apiKey)
51
52
  const parts = []
52
53
  if (result.user) parts.push(result.user)
53
54
  if (result.keyName) parts.push(`key: ${result.keyName}`)
@@ -56,7 +57,7 @@ auth
56
57
  console.error('Invalid API key')
57
58
  }
58
59
  } else {
59
- console.error('Please provide --api-key or --device option')
60
+ console.error('Please provide --api-key or --device option, or set HUM_API_KEY environment variable')
60
61
  }
61
62
  } catch (error) {
62
63
  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
 
@@ -1,168 +1,47 @@
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 sleep = new Command('sleep')
7
-
8
- sleep
9
- .command('add')
10
- .option('--duration <value>', 'Sleep duration in hours')
11
- .requiredOption('--bedtime <time>', 'Bedtime (HH:mm)')
12
- .requiredOption('--waketime <time>', 'Wake time (HH:mm)')
13
- .requiredOption('--quality <value>', 'Sleep quality 1-10')
14
- .option('--deep-sleep <value>', 'Deep sleep duration in hours')
15
- .option('--rem-sleep <value>', 'REM sleep duration in hours')
16
- .option('--awakenings <value>', 'Number of awakenings')
17
- .option('--feeling <value>', 'Feeling 1-10')
18
- .option('--extra-data <json>', 'Extra data (JSON string)')
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
- let duration = options.duration
25
- if (!duration && options.bedtime && options.waketime) {
26
- const [bh, bm] = options.bedtime.split(':').map(Number)
27
- const [wh, wm] = options.waketime.split(':').map(Number)
28
- let diff = (wh * 60 + wm) - (bh * 60 + bm)
29
- if (diff < 0) diff += 24 * 60
30
- duration = (diff / 60).toFixed(1)
31
- }
32
- if (!duration) {
33
- console.error('需要 --duration 或同时提供 --bedtime 和 --waketime')
34
- process.exit(1)
35
- }
36
-
37
- const formData = createFormData({
38
- duration,
39
- bedTime: options.bedtime,
40
- wakeTime: options.waketime,
41
- quality: options.quality,
42
- deepSleep: options.deepSleep,
43
- remSleep: options.remSleep,
44
- awakenings: options.awakenings,
45
- feeling: options.feeling,
46
- extraData: options.extraData,
47
- note: options.note,
48
- date: appendTimezoneOffset(options.date)
49
- }, options.file || [])
50
-
51
- const result = await request('/sleeps', {
52
- method: 'POST',
53
- body: formData,
54
- isFormData: true
55
- })
56
-
57
- console.log('Sleep record added:', result.id)
58
- } catch (error) {
59
- console.error('Failed to add sleep record:', error.message)
60
- }
61
- })
62
-
63
- sleep
64
- .command('list')
65
- .option('--last <period>', 'Last N days/weeks/months/years')
66
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
67
- .option('--end <date>', 'End date (YYYY-MM-DD)')
68
- .option('--page <number>', 'Page number', '1')
69
- .option('--limit <number>', 'Items per page', '20')
70
- .option('--include-deleted', 'Include deleted records')
71
- .option('--format <format>', 'Output format: json, table, toon', 'json')
72
- .action(async (options) => {
73
- try {
74
- const { params, page } = buildQueryParams(options)
75
- const result = await request(`/sleeps?${params.toString()}`)
76
- outputData(result, { format: options.format, type: 'sleep-list', page })
77
- } catch (error) {
78
- console.error('Failed to list sleep records:', error.message)
1
+ import { createCrudCommand } from '../lib/crud-command.js'
2
+
3
+ const sleep = createCrudCommand('sleep', {
4
+ endpoint: '/sleeps',
5
+ fields: [
6
+ { flag: 'duration', description: 'Sleep duration in hours' },
7
+ { flag: 'bedtime', description: 'Bedtime (HH:mm)', formKey: 'bedTime', required: true },
8
+ { flag: 'waketime', description: 'Wake time (HH:mm)', formKey: 'wakeTime', required: true },
9
+ { flag: 'quality', description: 'Sleep quality 1-10', required: true },
10
+ { flag: 'deep-sleep', description: 'Deep sleep duration in hours', formKey: 'deepSleep' },
11
+ { flag: 'rem-sleep', description: 'REM sleep duration in hours', formKey: 'remSleep' },
12
+ { flag: 'awakenings', description: 'Number of awakenings' },
13
+ { flag: 'feeling', description: 'Feeling 1-10' },
14
+ { flag: 'extra-data', description: 'Extra data (JSON string)', formKey: 'extraData' },
15
+ { flag: 'note', description: 'Note' }
16
+ ],
17
+ fileFields: ['file'],
18
+ beforeAdd(opts) {
19
+ let duration = opts.duration
20
+ if (!duration && opts.bedtime && opts.waketime) {
21
+ const [bh, bm] = opts.bedtime.split(':').map(Number)
22
+ const [wh, wm] = opts.waketime.split(':').map(Number)
23
+ let diff = (wh * 60 + wm) - (bh * 60 + bm)
24
+ if (diff < 0) diff += 24 * 60
25
+ duration = (diff / 60).toFixed(1)
79
26
  }
80
- })
81
-
82
- sleep
83
- .command('stats')
84
- .option('--last <period>', 'Last N days/weeks/months/years')
85
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
86
- .option('--end <date>', 'End date (YYYY-MM-DD)')
87
- .option('--format <format>', 'Output format: json, table, toon', 'json')
88
- .action(async (options) => {
89
- try {
90
- const { params } = buildQueryParams(options)
91
- const result = await request(`/sleeps/stats?${params.toString()}`)
92
- outputData(result, { format: options.format, type: 'sleep-stats' })
93
- } catch (error) {
94
- console.error('Failed to get sleep stats:', error.message)
27
+ if (!duration) {
28
+ console.error('需要 --duration 或同时提供 --bedtime 和 --waketime')
29
+ process.exit(1)
95
30
  }
96
- })
97
-
98
- sleep
99
- .command('get')
100
- .requiredOption('--id <id>', 'Sleep record ID')
101
- .option('--format <format>', 'Output format: json, table, toon', 'json')
102
- .action(async (options) => {
103
- try {
104
- const result = await request(`/sleeps/${options.id}`)
105
- outputData(result, { format: options.format, type: 'sleep-get' })
106
- } catch (error) {
107
- console.error('Failed to get sleep record:', error.message)
108
- }
109
- })
110
-
111
- sleep
112
- .command('update')
113
- .requiredOption('--id <id>', 'Sleep record ID')
114
- .option('--duration <value>', 'Updated duration')
115
- .option('--bedtime <time>', 'Updated bedtime')
116
- .option('--waketime <time>', 'Updated wake time')
117
- .option('--quality <value>', 'Updated quality')
118
- .option('--deep-sleep <value>', 'Updated deep sleep')
119
- .option('--rem-sleep <value>', 'Updated REM sleep')
120
- .option('--awakenings <value>', 'Updated number of awakenings')
121
- .option('--feeling <value>', 'Updated feeling')
122
- .option('--extra-data <json>', 'Updated extra data (JSON string)')
123
- .option('--note <note>', 'Updated note')
124
- .option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
125
- .option('--file <paths...>', 'File paths to attach')
126
- .option('--replace-attachments', 'Replace existing attachments instead of adding')
127
- .action(async (options) => {
128
- try {
129
- const formData = createFormData({
130
- duration: options.duration,
131
- bedTime: options.bedtime,
132
- wakeTime: options.waketime,
133
- quality: options.quality,
134
- deepSleep: options.deepSleep,
135
- remSleep: options.remSleep,
136
- awakenings: options.awakenings,
137
- feeling: options.feeling,
138
- extraData: options.extraData,
139
- note: options.note,
140
- date: appendTimezoneOffset(options.date),
141
- replaceAttachments: options.replaceAttachments ? 'true' : undefined
142
- }, options.file || [])
143
-
144
- const result = await request(`/sleeps/${options.id}`, {
145
- method: 'PATCH',
146
- body: formData,
147
- isFormData: true
148
- })
149
-
150
- console.log('Sleep record updated:', result.id)
151
- } catch (error) {
152
- console.error('Failed to update sleep record:', error.message)
153
- }
154
- })
155
-
156
- sleep
157
- .command('delete')
158
- .requiredOption('--id <id>', 'Sleep record ID')
159
- .action(async (options) => {
160
- try {
161
- await request(`/sleeps/${options.id}`, { method: 'DELETE' })
162
- console.log('Sleep record deleted')
163
- } catch (error) {
164
- console.error('Failed to delete sleep record:', error.message)
31
+ return {
32
+ duration,
33
+ bedTime: opts.bedtime,
34
+ wakeTime: opts.waketime,
35
+ quality: opts.quality,
36
+ deepSleep: opts.deepSleep,
37
+ remSleep: opts.remSleep,
38
+ awakenings: opts.awakenings,
39
+ feeling: opts.feeling,
40
+ extraData: opts.extraData,
41
+ note: opts.note,
42
+ date: opts.date
165
43
  }
166
- })
44
+ }
45
+ })
167
46
 
168
47
  export default sleep
@@ -19,7 +19,7 @@ timeline
19
19
  const result = await request(`/timeline?${params.toString()}`)
20
20
  outputData(result, { format: options.format, type: 'timeline', page })
21
21
  } catch (error) {
22
- console.error('Failed to get timeline:', error.message)
22
+ console.error('获取时间线失败:', error.message)
23
23
  }
24
24
  })
25
25
 
@@ -1,151 +1,19 @@
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 weight = new Command('weight')
7
-
8
- weight
9
- .command('add')
10
- .requiredOption('--value <value>', 'Weight value (kg)')
11
- .option('--body-fat <value>', 'Body fat percentage')
12
- .option('--muscle-mass <value>', 'Muscle mass (kg)')
13
- .option('--bmi <value>', 'BMI')
14
- .option('--water <value>', 'Water percentage')
15
- .option('--bone-mass <value>', 'Bone mass (kg)')
16
- .option('--visceral-fat <value>', 'Visceral fat level')
17
- .option('--extra-data <json>', 'Extra data (JSON string)')
18
- .option('--note <note>', 'Note for the record')
19
- .option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
20
- .option('--file <paths...>', 'File paths to attach')
21
- .action(async (options) => {
22
- try {
23
- const formData = createFormData({
24
- weight: options.value,
25
- bodyFat: options.bodyFat,
26
- muscleMass: options.muscleMass,
27
- bmi: options.bmi,
28
- water: options.water,
29
- boneMass: options.boneMass,
30
- visceralFat: options.visceralFat,
31
- extraData: options.extraData,
32
- note: options.note,
33
- date: appendTimezoneOffset(options.date)
34
- }, options.file || [])
35
-
36
- const result = await request('/weights', {
37
- method: 'POST',
38
- body: formData,
39
- isFormData: true
40
- })
41
-
42
- console.log('Weight record added:', result.id)
43
- } catch (error) {
44
- console.error('Failed to add weight record:', error.message)
45
- }
46
- })
47
-
48
- weight
49
- .command('list')
50
- .option('--last <period>', 'Last N days/weeks/months/years (e.g., 10, 7d, 2w, 6m, 1y)')
51
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
52
- .option('--end <date>', 'End date (YYYY-MM-DD)')
53
- .option('--page <number>', 'Page number', '1')
54
- .option('--limit <number>', 'Items per page', '20')
55
- .option('--include-deleted', 'Include deleted records')
56
- .option('--format <format>', 'Output format: json, table, toon', 'json')
57
- .action(async (options) => {
58
- try {
59
- const { params, page } = buildQueryParams(options)
60
- const result = await request(`/weights?${params.toString()}`)
61
- outputData(result, { format: options.format, type: 'weight-list', page })
62
- } catch (error) {
63
- console.error('Failed to list weight records:', error.message)
64
- }
65
- })
66
-
67
- weight
68
- .command('stats')
69
- .option('--last <period>', 'Last N days/weeks/months/years (e.g., 10, 7d, 2w, 6m, 1y)')
70
- .option('--start <date>', 'Start date (YYYY-MM-DD)')
71
- .option('--end <date>', 'End date (YYYY-MM-DD)')
72
- .option('--format <format>', 'Output format: json, table, toon', 'json')
73
- .action(async (options) => {
74
- try {
75
- const { params } = buildQueryParams(options)
76
- const result = await request(`/weights/stats?${params.toString()}`)
77
- outputData(result, { format: options.format, type: 'weight-stats' })
78
- } catch (error) {
79
- console.error('Failed to get weight stats:', error.message)
80
- }
81
- })
82
-
83
- weight
84
- .command('get')
85
- .requiredOption('--id <id>', 'Weight record ID')
86
- .option('--format <format>', 'Output format: json, table, toon', 'json')
87
- .action(async (options) => {
88
- try {
89
- const result = await request(`/weights/${options.id}`)
90
- outputData(result, { format: options.format, type: 'weight-get' })
91
- } catch (error) {
92
- console.error('Failed to get weight record:', error.message)
93
- }
94
- })
95
-
96
- weight
97
- .command('update')
98
- .requiredOption('--id <id>', 'Weight record ID')
99
- .option('--value <value>', 'Updated weight value (kg)')
100
- .option('--body-fat <value>', 'Updated body fat percentage')
101
- .option('--muscle-mass <value>', 'Updated muscle mass (kg)')
102
- .option('--bmi <value>', 'Updated BMI')
103
- .option('--water <value>', 'Updated water percentage')
104
- .option('--bone-mass <value>', 'Updated bone mass (kg)')
105
- .option('--visceral-fat <value>', 'Updated visceral fat level')
106
- .option('--extra-data <json>', 'Updated extra data (JSON string)')
107
- .option('--note <note>', 'Updated note')
108
- .option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
109
- .option('--file <paths...>', 'File paths to attach')
110
- .option('--replace-attachments', 'Replace existing attachments instead of adding')
111
- .action(async (options) => {
112
- try {
113
- const formData = createFormData({
114
- weight: options.value,
115
- bodyFat: options.bodyFat,
116
- muscleMass: options.muscleMass,
117
- bmi: options.bmi,
118
- water: options.water,
119
- boneMass: options.boneMass,
120
- visceralFat: options.visceralFat,
121
- extraData: options.extraData,
122
- note: options.note,
123
- date: appendTimezoneOffset(options.date),
124
- replaceAttachments: options.replaceAttachments ? 'true' : undefined
125
- }, options.file || [])
126
-
127
- const result = await request(`/weights/${options.id}`, {
128
- method: 'PATCH',
129
- body: formData,
130
- isFormData: true
131
- })
132
-
133
- console.log('Weight record updated:', result.id)
134
- } catch (error) {
135
- console.error('Failed to update weight record:', error.message)
136
- }
137
- })
138
-
139
- weight
140
- .command('delete')
141
- .requiredOption('--id <id>', 'Weight record ID')
142
- .action(async (options) => {
143
- try {
144
- await request(`/weights/${options.id}`, { method: 'DELETE' })
145
- console.log('Weight record deleted')
146
- } catch (error) {
147
- console.error('Failed to delete weight record:', error.message)
148
- }
149
- })
1
+ import { createCrudCommand } from '../lib/crud-command.js'
2
+
3
+ const weight = createCrudCommand('weight', {
4
+ endpoint: '/weights',
5
+ fields: [
6
+ { flag: 'value', description: 'Weight value (kg)', formKey: 'weight', required: true },
7
+ { flag: 'body-fat', description: 'Body fat percentage', formKey: 'bodyFat' },
8
+ { flag: 'muscle-mass', description: 'Muscle mass (kg)', formKey: 'muscleMass' },
9
+ { flag: 'bmi', description: 'BMI' },
10
+ { flag: 'water', description: 'Water percentage' },
11
+ { flag: 'bone-mass', description: 'Bone mass (kg)', formKey: 'boneMass' },
12
+ { flag: 'visceral-fat', description: 'Visceral fat level', formKey: 'visceralFat' },
13
+ { flag: 'extra-data', description: 'Extra data (JSON string)', formKey: 'extraData' },
14
+ { flag: 'note', description: 'Note for the record' }
15
+ ],
16
+ fileFields: ['file']
17
+ })
150
18
 
151
19
  export default weight
package/src/lib/api.js CHANGED
@@ -1,10 +1,19 @@
1
1
  import { readFileSync } from 'fs'
2
2
  import config from './config.js'
3
3
 
4
+ function yellowWarn(message) {
5
+ // \x1b[33m 是黄色,\x1b[0m 是重置
6
+ console.warn('\x1b[33m%s\x1b[0m', message)
7
+ }
8
+
4
9
  export async function request(endpoint, options = {}) {
5
10
  const apiUrl = config.get('apiUrl') || 'http://localhost:3000'
6
11
  const apiKey = config.get('apiKey')
7
12
 
13
+ if (apiUrl === 'http://localhost:3000') {
14
+ yellowWarn('Warning: Using local API (http://localhost:3000). Ensure this is intended.')
15
+ }
16
+
8
17
  const url = `${apiUrl}/api/v1${endpoint}`
9
18
  const headers = {
10
19
  ...options.headers
@@ -25,7 +34,7 @@ export async function request(endpoint, options = {}) {
25
34
 
26
35
  if (!response.ok) {
27
36
  const error = await response.json().catch(() => ({}))
28
- throw new Error(error.message || `HTTP error! status: ${response.status}`)
37
+ throw new Error(error.message || `请求失败,状态码: ${response.status}`)
29
38
  }
30
39
 
31
40
  return response.json()
@@ -0,0 +1,186 @@
1
+ import { Command } from 'commander'
2
+ import { request, createFormData } from './api.js'
3
+ import { appendTimezoneOffset, buildQueryParams } from './timezone.js'
4
+ import { outputData } from './output.js'
5
+
6
+ /**
7
+ * Create a CRUD command with add/list/get/update/delete/stats subcommands.
8
+ *
9
+ * @param {string} name - Command name (e.g. 'diet', 'exercise')
10
+ * @param {object} options
11
+ * @param {string} options.endpoint - API endpoint prefix (e.g. '/diets')
12
+ * @param {Array<{flag: string, description: string, formKey?: string, required?: boolean}>} options.fields
13
+ * CLI flags for add/update. `formKey` maps to the FormData field name; defaults to camelCase of flag.
14
+ * @param {string[]} [options.fileFields] - Flags that accept file paths (e.g. ['file'])
15
+ * @param {Function} [options.statsFormatter] - Optional formatter for stats output type (e.g. () => 'diet-stats')
16
+ * @param {Function} [options.beforeAdd] - Hook to mutate fields before add request
17
+ * @param {Function} [options.beforeUpdate] - Hook to mutate fields before update request
18
+ */
19
+ export function createCrudCommand(name, options) {
20
+ const { endpoint, fields = [], fileFields = [], statsFormatter, beforeAdd, beforeUpdate } = options
21
+ const cmd = new Command(name)
22
+
23
+ const toCamel = (str) =>
24
+ str.replace(/^-+/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase())
25
+
26
+ const buildFormData = (opts) => {
27
+ const data = {}
28
+ for (const f of fields) {
29
+ const key = f.formKey || toCamel(f.flag)
30
+ const val = opts[toCamel(f.flag)]
31
+ if (val !== undefined && val !== null) {
32
+ data[key] = val
33
+ }
34
+ }
35
+ if (opts.date) {
36
+ data.date = appendTimezoneOffset(opts.date)
37
+ }
38
+ if (opts.replaceAttachments) {
39
+ data.replaceAttachments = 'true'
40
+ }
41
+ const files = []
42
+ for (const ff of fileFields) {
43
+ const val = opts[toCamel(ff)]
44
+ if (val && val.length > 0) {
45
+ files.push(...val)
46
+ }
47
+ }
48
+ return createFormData(data, files)
49
+ }
50
+
51
+ // add
52
+ const addCmd = cmd.command('add')
53
+ for (const f of fields) {
54
+ const method = f.required ? 'requiredOption' : 'option'
55
+ addCmd[method](`--${f.flag} <value>`, f.description)
56
+ }
57
+ for (const ff of fileFields) {
58
+ addCmd.option(`--${ff} <paths...>`, 'File paths to attach')
59
+ }
60
+ addCmd
61
+ .option('--date <date>', 'Date (YYYY-MM-DD or ISO 8601 datetime)')
62
+ .action(async (opts) => {
63
+ try {
64
+ let formData = buildFormData(opts)
65
+ if (beforeAdd) {
66
+ const override = beforeAdd(opts)
67
+ if (override) {
68
+ formData = createFormData(override, opts.file || [])
69
+ }
70
+ }
71
+ const result = await request(endpoint, {
72
+ method: 'POST',
73
+ body: formData,
74
+ isFormData: true
75
+ })
76
+ console.log(`${name} record added:`, result.id)
77
+ } catch (error) {
78
+ console.error(`添加${name}记录失败:`, error.message)
79
+ process.exitCode = 1
80
+ }
81
+ })
82
+
83
+ // list
84
+ cmd
85
+ .command('list')
86
+ .option('--last <period>', 'Last N days/weeks/months/years')
87
+ .option('--start <date>', 'Start date (YYYY-MM-DD)')
88
+ .option('--end <date>', 'End date (YYYY-MM-DD)')
89
+ .option('--page <number>', 'Page number', '1')
90
+ .option('--limit <number>', 'Items per page', '20')
91
+ .option('--include-deleted', 'Include deleted records')
92
+ .option('--format <format>', 'Output format: json, table, toon', 'json')
93
+ .action(async (opts) => {
94
+ try {
95
+ const { params, page } = buildQueryParams(opts)
96
+ const result = await request(`${endpoint}?${params.toString()}`)
97
+ outputData(result, { format: opts.format, type: `${name}-list`, page })
98
+ } catch (error) {
99
+ console.error(`获取${name}列表失败:`, error.message)
100
+ process.exitCode = 1
101
+ }
102
+ })
103
+
104
+ // stats
105
+ cmd
106
+ .command('stats')
107
+ .option('--last <period>', 'Last N days/weeks/months/years')
108
+ .option('--start <date>', 'Start date (YYYY-MM-DD)')
109
+ .option('--end <date>', 'End date (YYYY-MM-DD)')
110
+ .option('--format <format>', 'Output format: json, table, toon', 'json')
111
+ .action(async (opts) => {
112
+ try {
113
+ const { params } = buildQueryParams(opts)
114
+ const result = await request(`${endpoint}/stats?${params.toString()}`)
115
+ const type = statsFormatter ? statsFormatter() : `${name}-stats`
116
+ outputData(result, { format: opts.format, type })
117
+ } catch (error) {
118
+ console.error(`获取${name}统计失败:`, error.message)
119
+ process.exitCode = 1
120
+ }
121
+ })
122
+
123
+ // get
124
+ cmd
125
+ .command('get')
126
+ .requiredOption('--id <id>', `${name} record ID`)
127
+ .option('--format <format>', 'Output format: json, table, toon', 'json')
128
+ .action(async (opts) => {
129
+ try {
130
+ const result = await request(`${endpoint}/${opts.id}`)
131
+ outputData(result, { format: opts.format, type: `${name}-get` })
132
+ } catch (error) {
133
+ console.error(`获取${name}记录失败:`, error.message)
134
+ process.exitCode = 1
135
+ }
136
+ })
137
+
138
+ // update
139
+ const updateCmd = cmd.command('update')
140
+ updateCmd.requiredOption('--id <id>', `${name} record ID`)
141
+ for (const f of fields) {
142
+ updateCmd.option(`--${f.flag} <value>`, `Updated ${f.description.toLowerCase()}`)
143
+ }
144
+ for (const ff of fileFields) {
145
+ updateCmd.option(`--${ff} <paths...>`, 'File paths to attach')
146
+ }
147
+ updateCmd
148
+ .option('--date <date>', 'Updated date (YYYY-MM-DD or ISO 8601 datetime)')
149
+ .option('--replace-attachments', 'Replace existing attachments instead of adding')
150
+ .action(async (opts) => {
151
+ try {
152
+ let formData = buildFormData(opts)
153
+ if (beforeUpdate) {
154
+ const override = beforeUpdate(opts)
155
+ if (override) {
156
+ formData = createFormData(override, opts.file || [])
157
+ }
158
+ }
159
+ const result = await request(`${endpoint}/${opts.id}`, {
160
+ method: 'PATCH',
161
+ body: formData,
162
+ isFormData: true
163
+ })
164
+ console.log(`${name} record updated:`, result.id)
165
+ } catch (error) {
166
+ console.error(`更新${name}记录失败:`, error.message)
167
+ process.exitCode = 1
168
+ }
169
+ })
170
+
171
+ // delete
172
+ cmd
173
+ .command('delete')
174
+ .requiredOption('--id <id>', `${name} record ID`)
175
+ .action(async (opts) => {
176
+ try {
177
+ await request(`${endpoint}/${opts.id}`, { method: 'DELETE' })
178
+ console.log(`${name} record deleted`)
179
+ } catch (error) {
180
+ console.error(`删除${name}记录失败:`, error.message)
181
+ process.exitCode = 1
182
+ }
183
+ })
184
+
185
+ return cmd
186
+ }
@@ -0,0 +1,200 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ CLI="node $(dirname "$0")/../bin/index.js"
5
+ API_URL="http://localhost:3001"
6
+ API_KEY="${HUM_API_KEY:-abc123}"
7
+
8
+ echo "=== Hum CLI Extended E2E Test ==="
9
+ echo ""
10
+
11
+ # 1. Config
12
+ echo "=== Config ==="
13
+ echo "1. Testing config set..."
14
+ $CLI config set apiUrl $API_URL
15
+ echo " ✓ apiUrl set"
16
+
17
+ echo "2. Testing config get..."
18
+ $CLI config get apiUrl > /dev/null
19
+ echo " ✓ config get ok"
20
+
21
+ echo "3. Testing config list..."
22
+ RESULT=$($CLI config list)
23
+ echo " $RESULT"
24
+ echo " ✓ config list ok"
25
+
26
+ # 2. Auth
27
+ echo ""
28
+ echo "=== Auth ==="
29
+ echo "4. Testing auth login with env var..."
30
+ HUM_API_KEY=$API_KEY $CLI auth login --api-key $API_KEY
31
+ echo " ✓ logged in"
32
+
33
+ echo "5. Testing auth status..."
34
+ $CLI auth status
35
+ echo " ✓ status ok"
36
+
37
+ # 3. Weight CRUD
38
+ echo ""
39
+ echo "=== Weight ==="
40
+ echo "6. Testing weight add..."
41
+ RESULT=$($CLI weight add --weight 70.5 --date "2026-05-28" 2&1)
42
+ echo " $RESULT"
43
+ WEIGHT_ID=$(echo "$RESULT" | grep -o '[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}' | head -1)
44
+ if [ -z "$WEIGHT_ID" ]; then
45
+ echo " ✗ failed to get weight id"
46
+ exit 1
47
+ fi
48
+ echo " ✓ weight added: $WEIGHT_ID"
49
+
50
+ echo "7. Testing weight list..."
51
+ $CLI weight list --last 7d > /dev/null
52
+ echo " ✓ weight list ok"
53
+
54
+ echo "8. Testing weight get..."
55
+ $CLI weight get $WEIGHT_ID > /dev/null
56
+ echo " ✓ weight get ok"
57
+
58
+ echo "9. Testing weight update..."
59
+ $CLI weight update $WEIGHT_ID --weight 71.0 > /dev/null
60
+ echo " ✓ weight update ok"
61
+
62
+ echo "10. Testing weight stats..."
63
+ $CLI weight stats --last 30d > /dev/null
64
+ echo " ✓ weight stats ok"
65
+
66
+ echo "11. Testing weight delete..."
67
+ $CLI weight delete $WEIGHT_ID > /dev/null
68
+ echo " ✓ weight delete ok"
69
+
70
+ # 4. Exercise CRUD
71
+ echo ""
72
+ echo "=== Exercise ==="
73
+ echo "12. Testing exercise add..."
74
+ RESULT=$($CLI exercise add --type running --duration 30 --date "2026-05-28" 2&1)
75
+ echo " $RESULT"
76
+ EXERCISE_ID=$(echo "$RESULT" | grep -o '[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}' | head -1)
77
+ if [ -z "$EXERCISE_ID" ]; then
78
+ echo " ✗ failed to get exercise id"
79
+ exit 1
80
+ fi
81
+ echo " ✓ exercise added: $EXERCISE_ID"
82
+
83
+ echo "13. Testing exercise list..."
84
+ $CLI exercise list --last 7d > /dev/null
85
+ echo " ✓ exercise list ok"
86
+
87
+ echo "14. Testing exercise stats..."
88
+ $CLI exercise stats --last 30d > /dev/null
89
+ echo " ✓ exercise stats ok"
90
+
91
+ echo "15. Testing exercise delete..."
92
+ $CLI exercise delete $EXERCISE_ID > /dev/null
93
+ echo " ✓ exercise delete ok"
94
+
95
+ # 5. Diet CRUD
96
+ echo ""
97
+ echo "=== Diet ==="
98
+ echo "16. Testing diet add..."
99
+ RESULT=$($CLI diet add --meal breakfast --calories 500 --date "2026-05-28" 2&1)
100
+ echo " $RESULT"
101
+ DIET_ID=$(echo "$RESULT" | grep -o '[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}' | head -1)
102
+ if [ -z "$DIET_ID" ]; then
103
+ echo " ✗ failed to get diet id"
104
+ exit 1
105
+ fi
106
+ echo " ✓ diet added: $DIET_ID"
107
+
108
+ echo "17. Testing diet list..."
109
+ $CLI diet list --last 7d > /dev/null
110
+ echo " ✓ diet list ok"
111
+
112
+ echo "18. Testing diet stats..."
113
+ $CLI diet stats --last 30d > /dev/null
114
+ echo " ✓ diet stats ok"
115
+
116
+ echo "19. Testing diet delete..."
117
+ $CLI diet delete $DIET_ID > /dev/null
118
+ echo " ✓ diet delete ok"
119
+
120
+ # 6. Sleep CRUD
121
+ echo ""
122
+ echo "=== Sleep ==="
123
+ echo "20. Testing sleep add..."
124
+ RESULT=$($CLI sleep add --duration 7.5 --bedtime "23:00" --waketime "06:30" 2&1)
125
+ echo " $RESULT"
126
+ SLEEP_ID=$(echo "$RESULT" | grep -o '[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}' | head -1)
127
+ if [ -z "$SLEEP_ID" ]; then
128
+ echo " ✗ failed to get sleep id"
129
+ exit 1
130
+ fi
131
+ echo " ✓ sleep added: $SLEEP_ID"
132
+
133
+ echo "21. Testing sleep list..."
134
+ $CLI sleep list --last 7d > /dev/null
135
+ echo " ✓ sleep list ok"
136
+
137
+ echo "22. Testing sleep stats..."
138
+ $CLI sleep stats --last 30d > /dev/null
139
+ echo " ✓ sleep stats ok"
140
+
141
+ echo "23. Testing sleep delete..."
142
+ $CLI sleep delete $SLEEP_ID > /dev/null
143
+ echo " ✓ sleep delete ok"
144
+
145
+ # 7. Food
146
+ echo ""
147
+ echo "=== Food ==="
148
+ echo "24. Testing food search..."
149
+ $CLI food search "苹果" > /dev/null 2&1 || true
150
+ echo " ✓ food search ok"
151
+
152
+ # 8. Record CRUD (original)
153
+ echo ""
154
+ echo "=== Record ==="
155
+ echo "25. Testing record add..."
156
+ RESULT=$($CLI record add --type custom --data '{"test":true}' --tags e2e --note "auto test" 2&1)
157
+ echo " $RESULT"
158
+ RECORD_ID=$(echo "$RESULT" | grep -o '[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}' | head -1)
159
+ if [ -z "$RECORD_ID" ]; then
160
+ echo " ✗ failed to get record id"
161
+ exit 1
162
+ fi
163
+ echo " ✓ record added: $RECORD_ID"
164
+
165
+ echo "26. Testing record get..."
166
+ $CLI record get --id $RECORD_ID > /dev/null
167
+ echo " ✓ record get ok"
168
+
169
+ echo "27. Testing record list..."
170
+ $CLI record list --tag e2e > /dev/null
171
+ echo " ✓ record list ok"
172
+
173
+ echo "28. Testing record update..."
174
+ $CLI record update --id $RECORD_ID --data '{"test":false}' > /dev/null
175
+ echo " ✓ record update ok"
176
+
177
+ echo "29. Testing record search..."
178
+ $CLI record search --query "auto test" > /dev/null
179
+ echo " ✓ record search ok"
180
+
181
+ echo "30. Testing record delete..."
182
+ $CLI record delete --id $RECORD_ID > /dev/null
183
+ echo " ✓ record delete ok"
184
+
185
+ # 9. Timeline
186
+ echo ""
187
+ echo "=== Timeline ==="
188
+ echo "31. Testing timeline..."
189
+ $CLI timeline --last 7d > /dev/null
190
+ echo " ✓ timeline ok"
191
+
192
+ # 10. Auth logout
193
+ echo ""
194
+ echo "=== Auth Logout ==="
195
+ echo "32. Testing auth logout..."
196
+ $CLI auth logout
197
+ echo " ✓ logout ok"
198
+
199
+ echo ""
200
+ echo "=== All extended tests passed! ==="