@cyt528300/reminders-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +43 -0
  2. package/bin/reminders.js +387 -0
  3. package/package.json +20 -0
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Reminders CLI
2
+
3
+ 命令行版本的 Reminders 客户端。首次使用需要登录,输入 API 域名和邀请码 code;登录成功后会把配置保存到 `~/.reminders-cli/config.json`。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g @cyt528300/reminders-cli
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ```bash
14
+ reminders login
15
+ reminders list
16
+ reminders create --title "买牛奶" --due-date 2026-07-21 --tags life,home
17
+ reminders update --id <id> --completed true
18
+ reminders delete --id <id>
19
+ reminders help create
20
+ ```
21
+
22
+ 也可以非交互登录:
23
+
24
+ ```bash
25
+ reminders login --api-url https://your-domain.example --code your-code
26
+ ```
27
+
28
+ ## 命令
29
+
30
+ - `login`: 输入 API 域名和邀请码 code,验证后保存到本地。
31
+ - `list`: 获取提醒事项列表。
32
+ - `create`: 创建提醒事项。
33
+ - `update`: 更新提醒事项。
34
+ - `delete`: 删除提醒事项。
35
+ - `help`: 查看每个函数的用法和返回值含义。
36
+
37
+ ## 发布
38
+
39
+ ```bash
40
+ cd packages/reminders-cli
41
+ npm login
42
+ npm publish --access public
43
+ ```
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs')
4
+ const os = require('os')
5
+ const path = require('path')
6
+ const readline = require('readline')
7
+
8
+ const DEFAULT_API_URL = process.env.REMINDERS_API_URL || 'http://localhost:3000'
9
+ const CONFIG_DIR = path.join(os.homedir(), '.reminders-cli')
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
11
+
12
+ const HELP = {
13
+ overview: `
14
+ Reminders CLI
15
+
16
+ 用法:
17
+ reminders <command> [options]
18
+
19
+ 命令:
20
+ login 首次登录,输入 API 域名和邀请码 code 并保存到本地
21
+ list 获取提醒事项列表
22
+ create 创建提醒事项
23
+ update 更新提醒事项
24
+ delete 删除提醒事项
25
+ help 查看命令帮助和返回值含义
26
+
27
+ 全局选项:
28
+ --api-url <url> API 服务地址;login 时可跳过交互输入
29
+ --json 原样输出 JSON 响应
30
+
31
+ 示例:
32
+ reminders login
33
+ reminders list
34
+ reminders create --title "买牛奶" --due-date 2026-07-21 --tags life,home
35
+ reminders update --id <id> --completed true
36
+ reminders delete --id <id>
37
+ reminders help create
38
+ `,
39
+ login: `
40
+ login
41
+
42
+ 用法:
43
+ reminders login [--api-url <url>] [--code <code>]
44
+
45
+ 说明:
46
+ 输入 API 域名和邀请码 code,验证成功后保存到 ${CONFIG_FILE}。
47
+ 之后 list/create/update/delete 会自动读取本地 code。
48
+
49
+ 返回值:
50
+ success=true 表示登录成功,并保存 apiUrl。
51
+ 401 表示缺少邀请码,403 表示邀请码无效。
52
+ `,
53
+ list: `
54
+ list
55
+
56
+ 用法:
57
+ reminders list [--json]
58
+
59
+ 说明:
60
+ 获取最多 1000 条提醒事项。
61
+
62
+ 返回值:
63
+ success: 是否成功
64
+ data: Reminder 数组
65
+ count: 返回条数
66
+ `,
67
+ create: `
68
+ create
69
+
70
+ 用法:
71
+ reminders create --title <title> [options]
72
+
73
+ 选项:
74
+ --title <text> 标题,必填
75
+ --notes <text> 备注
76
+ --due-date <iso/date> 到期时间,例如 2026-07-21 或 ISO 时间戳
77
+ --priority <value> low | medium | high
78
+ --tags <a,b,c> 逗号分隔的标签
79
+ --rank <number> 排序值,默认 0
80
+ --completed <boolean> true | false,默认 false
81
+ --json 原样输出 JSON 响应
82
+
83
+ 返回值:
84
+ success: 是否成功
85
+ data: 新创建的 Reminder
86
+ `,
87
+ update: `
88
+ update
89
+
90
+ 用法:
91
+ reminders update --id <id> [options]
92
+
93
+ 选项:
94
+ --id <id> 提醒事项 ID,必填
95
+ --title <text> 新标题
96
+ --notes <text> 新备注
97
+ --due-date <iso/date> 新到期时间
98
+ --priority <value> low | medium | high
99
+ --tags <a,b,c> 覆盖标签
100
+ --rank <number> 新排序值
101
+ --completed <boolean> true | false
102
+ --json 原样输出 JSON 响应
103
+
104
+ 返回值:
105
+ success: 是否成功
106
+ data: 更新后的 Reminder
107
+ 400: 缺少 id
108
+ 404: 未找到对应提醒事项
109
+ `,
110
+ delete: `
111
+ delete
112
+
113
+ 用法:
114
+ reminders delete --id <id> [--json]
115
+
116
+ 说明:
117
+ 删除指定提醒事项。
118
+
119
+ 返回值:
120
+ success: 是否成功
121
+ message: 删除结果说明
122
+ data.id: 已删除的提醒事项 ID
123
+ 400: 缺少 id
124
+ 404: 未找到对应提醒事项
125
+ `,
126
+ }
127
+
128
+ function parseArgs(argv) {
129
+ const args = { _: [] }
130
+
131
+ for (let i = 0; i < argv.length; i += 1) {
132
+ const token = argv[i]
133
+
134
+ if (!token.startsWith('--')) {
135
+ args._.push(token)
136
+ continue
137
+ }
138
+
139
+ const eqIndex = token.indexOf('=')
140
+ if (eqIndex !== -1) {
141
+ args[token.slice(2, eqIndex)] = token.slice(eqIndex + 1)
142
+ continue
143
+ }
144
+
145
+ const key = token.slice(2)
146
+ const next = argv[i + 1]
147
+ if (next && !next.startsWith('--')) {
148
+ args[key] = next
149
+ i += 1
150
+ } else {
151
+ args[key] = true
152
+ }
153
+ }
154
+
155
+ return args
156
+ }
157
+
158
+ function normalizeApiUrl(value) {
159
+ return String(value || DEFAULT_API_URL).trim().replace(/\/+$/, '')
160
+ }
161
+
162
+ function readConfig() {
163
+ if (!fs.existsSync(CONFIG_FILE)) return null
164
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
165
+ }
166
+
167
+ function writeConfig(config) {
168
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
169
+ fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
170
+ }
171
+
172
+ function prompt(question) {
173
+ const rl = readline.createInterface({
174
+ input: process.stdin,
175
+ output: process.stdout,
176
+ })
177
+
178
+ return new Promise(resolve => {
179
+ rl.question(question, answer => {
180
+ rl.close()
181
+ resolve(answer.trim())
182
+ })
183
+ })
184
+ }
185
+
186
+ function toBoolean(value, name) {
187
+ if (value === undefined) return undefined
188
+ if (value === true || value === 'true') return true
189
+ if (value === false || value === 'false') return false
190
+ throw new Error(`${name} 只支持 true 或 false`)
191
+ }
192
+
193
+ function toNumber(value, name) {
194
+ if (value === undefined) return undefined
195
+ const parsed = Number(value)
196
+ if (!Number.isFinite(parsed)) throw new Error(`${name} 必须是数字`)
197
+ return parsed
198
+ }
199
+
200
+ function parseTags(value) {
201
+ if (value === undefined) return undefined
202
+ return String(value)
203
+ .split(',')
204
+ .map(tag => tag.trim())
205
+ .filter(Boolean)
206
+ }
207
+
208
+ function requireLogin(args) {
209
+ const config = readConfig()
210
+ if (!config?.code) {
211
+ throw new Error('尚未登录,请先运行 reminders login')
212
+ }
213
+
214
+ return {
215
+ apiUrl: normalizeApiUrl(args['api-url'] || config.apiUrl),
216
+ code: config.code,
217
+ }
218
+ }
219
+
220
+ function buildUrl(apiUrl, pathname, code, params = {}) {
221
+ const url = new URL(pathname, `${apiUrl}/`)
222
+ url.searchParams.set('inviteCode', code)
223
+
224
+ Object.entries(params).forEach(([key, value]) => {
225
+ if (value !== undefined && value !== null) {
226
+ url.searchParams.set(key, String(value))
227
+ }
228
+ })
229
+
230
+ return url
231
+ }
232
+
233
+ async function requestJson(url, options = {}) {
234
+ const response = await fetch(url, options)
235
+ const text = await response.text()
236
+ let body
237
+
238
+ try {
239
+ body = text ? JSON.parse(text) : {}
240
+ } catch (error) {
241
+ throw new Error(`响应不是有效 JSON: ${text}`)
242
+ }
243
+
244
+ if (!response.ok) {
245
+ const message = body.details || body.error || response.statusText
246
+ throw new Error(`HTTP ${response.status}: ${message}`)
247
+ }
248
+
249
+ return body
250
+ }
251
+
252
+ function printResult(result, rawJson) {
253
+ if (rawJson) {
254
+ console.log(JSON.stringify(result, null, 2))
255
+ return
256
+ }
257
+
258
+ if (Array.isArray(result.data)) {
259
+ console.log(`成功: ${result.success}`)
260
+ console.log(`数量: ${result.count}`)
261
+ result.data.forEach(item => {
262
+ const status = item.completed ? 'done' : 'todo'
263
+ const dueDate = item.dueDate ? ` due=${item.dueDate}` : ''
264
+ const tags = item.tags?.length ? ` tags=${item.tags.join(',')}` : ''
265
+ console.log(`- ${item.id} [${status}] ${item.title}${dueDate}${tags}`)
266
+ })
267
+ return
268
+ }
269
+
270
+ console.log(JSON.stringify(result, null, 2))
271
+ }
272
+
273
+ async function login(args) {
274
+ const apiUrlAnswer = args['api-url'] || await prompt(`请输入 API 域名 [${DEFAULT_API_URL}]: `)
275
+ const apiUrl = normalizeApiUrl(apiUrlAnswer || DEFAULT_API_URL)
276
+ const code = args.code || await prompt('请输入邀请码 code: ')
277
+
278
+ if (!code) throw new Error('code 不能为空')
279
+
280
+ const result = await requestJson(buildUrl(apiUrl, '/api/list', code))
281
+ writeConfig({ apiUrl, code, loggedInAt: new Date().toISOString() })
282
+
283
+ printResult({
284
+ success: true,
285
+ apiUrl,
286
+ message: '登录成功,code 已保存到本地',
287
+ verify: { count: result.count },
288
+ }, args.json)
289
+ }
290
+
291
+ async function list(args) {
292
+ const { apiUrl, code } = requireLogin(args)
293
+ const result = await requestJson(buildUrl(apiUrl, '/api/list', code))
294
+ printResult(result, args.json)
295
+ }
296
+
297
+ async function create(args) {
298
+ const { apiUrl, code } = requireLogin(args)
299
+
300
+ if (!args.title) throw new Error('create 需要 --title')
301
+
302
+ const now = new Date().toISOString()
303
+ const payload = {
304
+ title: args.title,
305
+ notes: args.notes,
306
+ dueDate: args['due-date'],
307
+ priority: args.priority,
308
+ tags: parseTags(args.tags),
309
+ rank: toNumber(args.rank, '--rank') ?? 0,
310
+ completed: toBoolean(args.completed, '--completed') ?? false,
311
+ createdAt: now,
312
+ updatedAt: now,
313
+ }
314
+
315
+ const result = await requestJson(buildUrl(apiUrl, '/api/insert', code), {
316
+ method: 'POST',
317
+ headers: { 'Content-Type': 'application/json' },
318
+ body: JSON.stringify(payload),
319
+ })
320
+
321
+ printResult(result, args.json)
322
+ }
323
+
324
+ async function update(args) {
325
+ const { apiUrl, code } = requireLogin(args)
326
+
327
+ if (!args.id) throw new Error('update 需要 --id')
328
+
329
+ const payload = {
330
+ id: args.id,
331
+ title: args.title,
332
+ notes: args.notes,
333
+ dueDate: args['due-date'],
334
+ priority: args.priority,
335
+ tags: parseTags(args.tags),
336
+ rank: toNumber(args.rank, '--rank'),
337
+ completed: toBoolean(args.completed, '--completed'),
338
+ }
339
+
340
+ Object.keys(payload).forEach(key => {
341
+ if (payload[key] === undefined) delete payload[key]
342
+ })
343
+
344
+ const result = await requestJson(buildUrl(apiUrl, '/api/update', code), {
345
+ method: 'PUT',
346
+ headers: { 'Content-Type': 'application/json' },
347
+ body: JSON.stringify(payload),
348
+ })
349
+
350
+ printResult(result, args.json)
351
+ }
352
+
353
+ async function remove(args) {
354
+ const { apiUrl, code } = requireLogin(args)
355
+
356
+ if (!args.id) throw new Error('delete 需要 --id')
357
+
358
+ const result = await requestJson(buildUrl(apiUrl, '/api/delete', code, { id: args.id }), {
359
+ method: 'DELETE',
360
+ })
361
+
362
+ printResult(result, args.json)
363
+ }
364
+
365
+ function help(args) {
366
+ const topic = args._[1] || 'overview'
367
+ console.log((HELP[topic] || HELP.overview).trim())
368
+ }
369
+
370
+ async function main() {
371
+ const args = parseArgs(process.argv.slice(2))
372
+ const command = args._[0] || 'help'
373
+
374
+ if (command === 'help' || args.help) return help(args)
375
+ if (command === 'login') return login(args)
376
+ if (command === 'list') return list(args)
377
+ if (command === 'create') return create(args)
378
+ if (command === 'update') return update(args)
379
+ if (command === 'delete') return remove(args)
380
+
381
+ throw new Error(`未知命令: ${command}。运行 reminders help 查看用法。`)
382
+ }
383
+
384
+ main().catch(error => {
385
+ console.error(`错误: ${error.message}`)
386
+ process.exit(1)
387
+ })
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@cyt528300/reminders-cli",
3
+ "version": "0.1.0",
4
+ "description": "Command line client for the Reminders app API.",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "reminders": "bin/reminders.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ }
20
+ }