@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.
- package/README.md +43 -0
- package/bin/reminders.js +387 -0
- 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
|
+
```
|
package/bin/reminders.js
ADDED
|
@@ -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
|
+
}
|