@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.
@@ -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,9 +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')
12
+ const accessToken = config.get('accessToken')
13
+
14
+ if (apiUrl === 'http://localhost:3000') {
15
+ yellowWarn('Warning: Using local API (http://localhost:3000). Ensure this is intended.')
16
+ }
7
17
 
8
18
  const url = `${apiUrl}/api/v1${endpoint}`
9
19
  const headers = {
@@ -14,8 +24,9 @@ export async function request(endpoint, options = {}) {
14
24
  headers['Content-Type'] = 'application/json'
15
25
  }
16
26
 
17
- if (apiKey) {
18
- headers['Authorization'] = `Bearer ${apiKey}`
27
+ const bearerToken = apiKey || accessToken
28
+ if (bearerToken) {
29
+ headers['Authorization'] = `Bearer ${bearerToken}`
19
30
  }
20
31
 
21
32
  const response = await fetch(url, {
@@ -25,7 +36,7 @@ export async function request(endpoint, options = {}) {
25
36
 
26
37
  if (!response.ok) {
27
38
  const error = await response.json().catch(() => ({}))
28
- throw new Error(error.message || `HTTP error! status: ${response.status}`)
39
+ throw new Error(error.message || `请求失败,状态码: ${response.status}`)
29
40
  }
30
41
 
31
42
  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
+ }