@wangjs-jacky/ticktick-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 (40) hide show
  1. package/.github/workflows/npm-publish.yml +26 -0
  2. package/CLAUDE.md +34 -0
  3. package/README.md +62 -0
  4. package/README_CN.md +62 -0
  5. package/bin/cli.ts +2 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +1490 -0
  8. package/dist/index.js.map +1 -0
  9. package/docs/oauth-credential-pre-validation.md +253 -0
  10. package/docs/reference/cli-usage-guide.md +587 -0
  11. package/docs/reference/dida365-open-api-zh.md +999 -0
  12. package/docs/reference/dida365-open-api.md +999 -0
  13. package/docs/reference/project-guide.md +63 -0
  14. package/docs/superpowers/plans/2026-04-03-tt-cli-auth.md +1110 -0
  15. package/docs/superpowers/specs/2026-04-03-tt-cli-design.md +142 -0
  16. package/package.json +45 -0
  17. package/skills/tt-cli-guide/SKILL.md +152 -0
  18. package/skills/tt-cli-guide/references/intent-mapping.md +169 -0
  19. package/src/api/client.ts +61 -0
  20. package/src/api/oauth.ts +146 -0
  21. package/src/api/resources.ts +291 -0
  22. package/src/commands/auth.ts +218 -0
  23. package/src/commands/project.ts +303 -0
  24. package/src/commands/task.ts +806 -0
  25. package/src/commands/user.ts +43 -0
  26. package/src/index.ts +46 -0
  27. package/src/types.ts +211 -0
  28. package/src/utils/config.ts +88 -0
  29. package/src/utils/endpoints.ts +22 -0
  30. package/src/utils/format.ts +71 -0
  31. package/src/utils/server.ts +81 -0
  32. package/tests/config.test.ts +87 -0
  33. package/tests/format.test.ts +56 -0
  34. package/tests/oauth.test.ts +42 -0
  35. package/tests/parity-fields.test.ts +89 -0
  36. package/tests/parity-map.ts +184 -0
  37. package/tests/parity.test.ts +101 -0
  38. package/tsconfig.json +22 -0
  39. package/tsup.config.ts +12 -0
  40. package/vitest.config.ts +7 -0
@@ -0,0 +1,806 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import type {
4
+ Project,
5
+ Task,
6
+ CreateTaskParams,
7
+ UpdateTaskParams,
8
+ } from '../types.js';
9
+ import { formatTaskTime, normalizeTickTickDate } from '../utils/format.js';
10
+ import {
11
+ getProjects,
12
+ getTask,
13
+ createTask,
14
+ updateTask,
15
+ completeTask,
16
+ deleteTask,
17
+ moveTasks,
18
+ getCompletedTasks,
19
+ filterTasks,
20
+ batchAddTasks,
21
+ batchUpdateTasks,
22
+ completeTasksInProject,
23
+ getTaskById,
24
+ listUndoneTasksByDate,
25
+ listUndoneTasksByTimeQuery,
26
+ searchTask,
27
+ } from '../api/resources.js';
28
+
29
+ // ─── 格式化工具 ──────────────────────────────────────
30
+
31
+ function priorityText(priority?: number): string {
32
+ const v = priority ?? 0;
33
+ switch (v) {
34
+ case 5:
35
+ return pc.red('高');
36
+ case 3:
37
+ return pc.yellow('中');
38
+ case 1:
39
+ return pc.blue('低');
40
+ default:
41
+ return pc.dim('无');
42
+ }
43
+ }
44
+
45
+ function statusIcon(status?: number): string {
46
+ return status === 2 ? pc.green('✓') : '○';
47
+ }
48
+
49
+ function displayTaskDetail(task: Task): void {
50
+ console.log('');
51
+ console.log(` ${pc.bold(task.title)}`);
52
+ console.log(` ${pc.dim('─'.repeat(40))}`);
53
+ console.log(` ID: ${task.id}`);
54
+ console.log(` 项目: ${task.projectId}`);
55
+ console.log(` 优先级: ${priorityText(task.priority)}`);
56
+ console.log(` 状态: ${task.status === 2 ? pc.green('已完成') : '待办'}`);
57
+ if (task.isAllDay !== undefined)
58
+ console.log(` 全天: ${task.isAllDay ? '是' : '否'}`);
59
+ if (task.startDate)
60
+ console.log(` 开始: ${task.startDate}`);
61
+ if (task.dueDate)
62
+ console.log(` 截止: ${task.dueDate}`);
63
+ if (task.timeZone)
64
+ console.log(` 时区: ${task.timeZone}`);
65
+ if (task.kind)
66
+ console.log(` 类型: ${task.kind}`);
67
+ if (task.content) {
68
+ console.log(` 内容:`);
69
+ console.log(` ${task.content}`);
70
+ }
71
+ if (task.tags && task.tags.length > 0) {
72
+ console.log(` 标签: ${task.tags.join(', ')}`);
73
+ }
74
+ if (task.items && task.items.length > 0) {
75
+ console.log(` 子任务:`);
76
+ for (const item of task.items) {
77
+ const icon = item.status === 1 ? pc.green('✓') : '○';
78
+ console.log(` ${icon} ${item.title}`);
79
+ }
80
+ }
81
+ console.log('');
82
+ }
83
+
84
+ function displayTaskTable(tasks: Task[]): void {
85
+ if (tasks.length === 0) {
86
+ p.log.info(pc.dim(' (无任务)'));
87
+ return;
88
+ }
89
+
90
+ for (const task of tasks) {
91
+ const icon = statusIcon(task.status);
92
+ const title =
93
+ task.title.length > 30
94
+ ? task.title.slice(0, 30) + '…'
95
+ : task.title;
96
+ const time = formatTaskTime(task);
97
+ const timeStr = time ? pc.cyan(time) : '';
98
+ console.log(
99
+ ` ${icon} ${title} ${timeStr} ${pc.dim(task.id)} ${priorityText(task.priority)} ${pc.dim(task.projectId)}`
100
+ );
101
+ }
102
+ }
103
+
104
+ /** 交互式选择项目 */
105
+ async function selectProject(): Promise<string | undefined> {
106
+ const s = p.spinner();
107
+ s.start('正在获取项目列表...');
108
+ const projects = await getProjects();
109
+ s.stop('');
110
+
111
+ if (projects.length === 0) {
112
+ p.log.error('没有可用的项目,请先创建项目');
113
+ return undefined;
114
+ }
115
+
116
+ const selected = await p.select({
117
+ message: '选择项目',
118
+ options: projects.map((proj: Project) => ({
119
+ value: proj.id,
120
+ label: proj.name,
121
+ })),
122
+ });
123
+
124
+ if (p.isCancel(selected)) return undefined;
125
+ return selected as string;
126
+ }
127
+
128
+ /** 解析优先级字符串 */
129
+ function parsePriority(value: string | undefined): number | undefined {
130
+ if (!value) return undefined;
131
+ const n = parseInt(value, 10);
132
+ if ([0, 1, 3, 5].includes(n)) return n;
133
+ return undefined;
134
+ }
135
+
136
+ // ─── 命令实现 ────────────────────────────────────────
137
+
138
+ async function taskAddCommand(
139
+ title: string | undefined,
140
+ options: {
141
+ project?: string;
142
+ content?: string;
143
+ priority?: string;
144
+ startDate?: string;
145
+ dueDate?: string;
146
+ allDay?: boolean;
147
+ }
148
+ ): Promise<void> {
149
+ if (!title) {
150
+ const input = await p.text({ message: '请输入任务标题' });
151
+ if (p.isCancel(input)) {
152
+ p.outro('已取消');
153
+ return;
154
+ }
155
+ title = input;
156
+ }
157
+
158
+ let projectId = options.project;
159
+ if (!projectId) {
160
+ projectId = await selectProject();
161
+ if (!projectId) {
162
+ p.outro('已取消');
163
+ return;
164
+ }
165
+ }
166
+
167
+ const params: CreateTaskParams = { title, projectId };
168
+ if (options.content) params.content = options.content;
169
+ if (options.priority) {
170
+ const priority = parsePriority(options.priority);
171
+ if (priority !== undefined) params.priority = priority;
172
+ }
173
+ if (options.startDate) params.startDate = normalizeTickTickDate(options.startDate);
174
+ if (options.dueDate) params.dueDate = normalizeTickTickDate(options.dueDate);
175
+ if (options.allDay !== undefined) params.isAllDay = options.allDay;
176
+
177
+ const s = p.spinner();
178
+ s.start('正在创建任务...');
179
+
180
+ try {
181
+ const task = await createTask(params);
182
+ s.stop('创建成功');
183
+ p.outro(pc.green(`任务「${task.title}」已创建 (ID: ${task.id})`));
184
+ } catch (err) {
185
+ s.stop('创建失败');
186
+ p.outro(pc.red((err as Error).message));
187
+ }
188
+ }
189
+
190
+ async function taskGetCommand(
191
+ projectId: string,
192
+ taskId: string
193
+ ): Promise<void> {
194
+ const s = p.spinner();
195
+ s.start('正在获取任务...');
196
+
197
+ try {
198
+ const task = await getTask(projectId, taskId);
199
+ s.stop('获取成功');
200
+ displayTaskDetail(task);
201
+ p.outro('任务详情如上');
202
+ } catch (err) {
203
+ s.stop('获取失败');
204
+ const msg = (err as Error).message;
205
+ p.outro(
206
+ pc.red(`获取任务失败 (projectId: ${projectId}, taskId: ${taskId})\n`) +
207
+ pc.red(`${msg}\n`) +
208
+ pc.yellow('排查建议: 运行 tt task-find <taskId> 或 tt task-search <关键词> 验证任务')
209
+ );
210
+ }
211
+ }
212
+
213
+ async function taskDoneCommand(
214
+ projectId: string,
215
+ taskId: string
216
+ ): Promise<void> {
217
+ const s = p.spinner();
218
+ s.start('正在完成任务...');
219
+
220
+ try {
221
+ await completeTask(projectId, taskId);
222
+ s.stop('完成成功');
223
+ p.outro(pc.green('任务已完成'));
224
+ } catch (err) {
225
+ s.stop('操作失败');
226
+ const msg = (err as Error).message;
227
+ p.outro(
228
+ pc.red(`任务完成失败 (projectId: ${projectId}, taskId: ${taskId})\n`) +
229
+ pc.red(`${msg}\n`) +
230
+ pc.yellow('排查建议: 运行 tt task-search 检查任务是否存在或已完成')
231
+ );
232
+ }
233
+ }
234
+
235
+ async function taskDeleteCommand(
236
+ projectId: string,
237
+ taskId: string
238
+ ): Promise<void> {
239
+ const confirmed = await p.confirm({
240
+ message: `确认删除任务 ${pc.red(taskId)}?`,
241
+ });
242
+ if (p.isCancel(confirmed) || !confirmed) {
243
+ p.outro('已取消');
244
+ return;
245
+ }
246
+
247
+ const s = p.spinner();
248
+ s.start('正在删除任务...');
249
+
250
+ try {
251
+ await deleteTask(projectId, taskId);
252
+ s.stop('删除成功');
253
+ p.outro(pc.green('任务已删除'));
254
+ } catch (err) {
255
+ s.stop('删除失败');
256
+ const msg = (err as Error).message;
257
+ p.outro(
258
+ pc.red(`删除任务失败 (projectId: ${projectId}, taskId: ${taskId})\n`) +
259
+ pc.red(`${msg}\n`) +
260
+ pc.yellow('排查建议: 运行 tt task-get <projectId> <taskId> 验证任务是否存在')
261
+ );
262
+ }
263
+ }
264
+
265
+ async function taskUpdateCommand(
266
+ taskId: string,
267
+ options: {
268
+ project?: string;
269
+ title?: string;
270
+ content?: string;
271
+ priority?: string;
272
+ startDate?: string;
273
+ dueDate?: string;
274
+ }
275
+ ): Promise<void> {
276
+ if (!options.project) {
277
+ p.outro(pc.red('请使用 -p/--project 指定项目 ID'));
278
+ return;
279
+ }
280
+
281
+ const params: UpdateTaskParams = {
282
+ id: taskId,
283
+ projectId: options.project,
284
+ };
285
+ if (options.title) params.title = options.title;
286
+ if (options.content) params.content = options.content;
287
+ if (options.priority) {
288
+ const priority = parsePriority(options.priority);
289
+ if (priority !== undefined) params.priority = priority;
290
+ }
291
+ if (options.startDate) params.startDate = normalizeTickTickDate(options.startDate);
292
+ if (options.dueDate) params.dueDate = normalizeTickTickDate(options.dueDate);
293
+
294
+ const s = p.spinner();
295
+ s.start('正在更新任务...');
296
+
297
+ try {
298
+ const task = await updateTask(taskId, params);
299
+ s.stop('更新成功');
300
+ p.outro(pc.green(`任务「${task.title}」已更新`));
301
+ } catch (err) {
302
+ s.stop('更新失败');
303
+ const msg = (err as Error).message;
304
+ p.outro(
305
+ pc.red(`更新任务失败 (taskId: ${taskId}, projectId: ${options.project})\n`) +
306
+ pc.red(`${msg}\n`) +
307
+ pc.yellow('排查建议: 运行 tt task-find <taskId> 验证任务是否存在')
308
+ );
309
+ }
310
+ }
311
+
312
+ async function taskMoveCommand(
313
+ taskId: string,
314
+ options: {
315
+ from?: string;
316
+ to?: string;
317
+ }
318
+ ): Promise<void> {
319
+ if (!options.from || !options.to) {
320
+ p.outro(
321
+ pc.red('请使用 -f/--from 和 -t/--to 指定源和目标项目 ID')
322
+ );
323
+ return;
324
+ }
325
+
326
+ const s = p.spinner();
327
+ s.start('正在移动任务...');
328
+
329
+ try {
330
+ const result = await moveTasks([
331
+ {
332
+ fromProjectId: options.from,
333
+ toProjectId: options.to,
334
+ taskId,
335
+ },
336
+ ]);
337
+ s.stop('移动成功');
338
+ p.outro(
339
+ pc.green(
340
+ `任务已移动 (etag: ${result[0]?.etag ?? '-'})`
341
+ )
342
+ );
343
+ } catch (err) {
344
+ s.stop('移动失败');
345
+ const msg = (err as Error).message;
346
+ p.outro(
347
+ pc.red(`移动任务失败 (taskId: ${taskId}, from: ${options.from}, to: ${options.to})\n`) +
348
+ pc.red(`${msg}\n`) +
349
+ pc.yellow('排查建议: 运行 tt project-list 验证项目 ID 是否有效')
350
+ );
351
+ }
352
+ }
353
+
354
+ async function taskCompletedCommand(options: {
355
+ project?: string;
356
+ start?: string;
357
+ end?: string;
358
+ }): Promise<void> {
359
+ const params: {
360
+ projectIds?: string[];
361
+ startDate?: string;
362
+ endDate?: string;
363
+ } = {};
364
+
365
+ if (options.project) params.projectIds = [options.project];
366
+ if (options.start) params.startDate = options.start;
367
+ if (options.end) params.endDate = options.end;
368
+
369
+ const s = p.spinner();
370
+ s.start('正在获取已完成任务...');
371
+
372
+ try {
373
+ const tasks = await getCompletedTasks(params);
374
+ s.stop(`找到 ${tasks.length} 个已完成任务`);
375
+
376
+ if (tasks.length === 0) {
377
+ p.outro(pc.yellow('没有找到已完成的任务'));
378
+ return;
379
+ }
380
+
381
+ console.log('');
382
+ displayTaskTable(tasks);
383
+ console.log('');
384
+ p.outro(`共 ${tasks.length} 个已完成任务`);
385
+ } catch (err) {
386
+ s.stop('获取失败');
387
+ p.outro(pc.red((err as Error).message));
388
+ }
389
+ }
390
+
391
+ async function taskListCommand(options: {
392
+ project?: string;
393
+ start?: string;
394
+ end?: string;
395
+ status?: string;
396
+ priority?: string;
397
+ tag?: string;
398
+ json?: boolean;
399
+ }): Promise<void> {
400
+ const params: {
401
+ projectIds?: string[];
402
+ startDate?: string;
403
+ endDate?: string;
404
+ status?: number[];
405
+ priority?: number[];
406
+ tag?: string[];
407
+ } = {};
408
+
409
+ if (options.project) params.projectIds = [options.project];
410
+ if (options.start) params.startDate = options.start;
411
+ if (options.end) params.endDate = options.end;
412
+ if (options.status) {
413
+ params.status = options.status.split(',').map((s) => parseInt(s, 10));
414
+ }
415
+ if (options.priority) {
416
+ params.priority = options.priority
417
+ .split(',')
418
+ .map((s) => parseInt(s, 10));
419
+ }
420
+ if (options.tag) {
421
+ params.tag = options.tag.split(',');
422
+ }
423
+
424
+ try {
425
+ const tasks = await filterTasks(params);
426
+
427
+ if (options.json) {
428
+ console.log(JSON.stringify(tasks, null, 2));
429
+ return;
430
+ }
431
+
432
+ const s = p.spinner();
433
+ s.start('正在筛选任务...');
434
+ s.stop(`找到 ${tasks.length} 个任务`);
435
+
436
+ if (tasks.length === 0) {
437
+ p.outro(pc.yellow('没有找到匹配的任务'));
438
+ return;
439
+ }
440
+
441
+ console.log('');
442
+ displayTaskTable(tasks);
443
+ console.log('');
444
+ p.outro(`共 ${tasks.length} 个任务`);
445
+ } catch (err) {
446
+ p.outro(pc.red((err as Error).message));
447
+ }
448
+ }
449
+
450
+ // ─── 批量 / 高级命令 ────────────────────────────────
451
+
452
+ async function taskBatchAddCommand(
453
+ jsonFile: string | undefined,
454
+ options: { stdin?: boolean }
455
+ ): Promise<void> {
456
+ let jsonStr: string;
457
+
458
+ if (options.stdin || !jsonFile) {
459
+ // 从 stdin 读取
460
+ const chunks: Buffer[] = [];
461
+ for await (const chunk of process.stdin) {
462
+ chunks.push(chunk as Buffer);
463
+ }
464
+ jsonStr = Buffer.concat(chunks).toString('utf-8');
465
+ } else {
466
+ // 从文件读取
467
+ const fs = await import('fs/promises');
468
+ jsonStr = await fs.readFile(jsonFile, 'utf-8');
469
+ }
470
+
471
+ let tasks: CreateTaskParams[];
472
+ try {
473
+ tasks = JSON.parse(jsonStr);
474
+ if (!Array.isArray(tasks)) {
475
+ p.outro(pc.red('JSON 必须是任务数组'));
476
+ return;
477
+ }
478
+ } catch {
479
+ p.outro(pc.red('JSON 解析失败,请检查格式'));
480
+ return;
481
+ }
482
+
483
+ // 规范化日期格式
484
+ for (const task of tasks) {
485
+ if (task.startDate) task.startDate = normalizeTickTickDate(task.startDate);
486
+ if (task.dueDate) task.dueDate = normalizeTickTickDate(task.dueDate);
487
+ }
488
+
489
+ const s = p.spinner();
490
+ s.start(`正在批量创建 ${tasks.length} 个任务...`);
491
+
492
+ try {
493
+ const result = await batchAddTasks(tasks);
494
+ s.stop(`成功创建 ${result.count} 个任务`);
495
+ console.log('');
496
+ for (const task of tasks) {
497
+ console.log(` ${pc.green('✓')} ${task.title}`);
498
+ }
499
+ console.log('');
500
+ p.outro(`共创建 ${result.count} 个任务`);
501
+ } catch (err) {
502
+ s.stop('批量创建失败');
503
+ p.outro(pc.red((err as Error).message));
504
+ }
505
+ }
506
+
507
+ async function taskBatchUpdateCommand(
508
+ jsonFile: string | undefined,
509
+ options: { stdin?: boolean }
510
+ ): Promise<void> {
511
+ let jsonStr: string;
512
+
513
+ if (options.stdin || !jsonFile) {
514
+ const chunks: Buffer[] = [];
515
+ for await (const chunk of process.stdin) {
516
+ chunks.push(chunk as Buffer);
517
+ }
518
+ jsonStr = Buffer.concat(chunks).toString('utf-8');
519
+ } else {
520
+ const fs = await import('fs/promises');
521
+ jsonStr = await fs.readFile(jsonFile, 'utf-8');
522
+ }
523
+
524
+ let tasks: UpdateTaskParams[];
525
+ try {
526
+ tasks = JSON.parse(jsonStr);
527
+ if (!Array.isArray(tasks)) {
528
+ p.outro(pc.red('JSON 必须是任务数组'));
529
+ return;
530
+ }
531
+ } catch {
532
+ p.outro(pc.red('JSON 解析失败,请检查格式'));
533
+ return;
534
+ }
535
+
536
+ const s = p.spinner();
537
+ s.start(`正在批量更新 ${tasks.length} 个任务...`);
538
+
539
+ try {
540
+ await batchUpdateTasks(tasks);
541
+ s.stop(`成功更新 ${tasks.length} 个任务`);
542
+ p.outro(pc.green(`已更新 ${tasks.length} 个任务`));
543
+ } catch (err) {
544
+ s.stop('批量更新失败');
545
+ p.outro(pc.red((err as Error).message));
546
+ }
547
+ }
548
+
549
+ async function taskBatchDoneCommand(
550
+ projectId: string,
551
+ options: {
552
+ taskIds?: string;
553
+ all?: boolean;
554
+ force?: boolean;
555
+ }
556
+ ): Promise<void> {
557
+ let ids: string[];
558
+
559
+ if (options.all) {
560
+ // 获取项目下所有未完成任务
561
+ const s = p.spinner();
562
+ s.start('正在获取项目任务...');
563
+ const tasks = await filterTasks({ projectIds: [projectId], status: [0] });
564
+ s.stop(`找到 ${tasks.length} 个未完成任务`);
565
+ ids = tasks.map((t) => t.id);
566
+ } else if (options.taskIds) {
567
+ ids = options.taskIds.split(',').map((s) => s.trim());
568
+ } else {
569
+ p.outro(pc.red('请使用 --task-ids <id1,id2,...> 或 --all'));
570
+ return;
571
+ }
572
+
573
+ if (ids.length === 0) {
574
+ p.outro(pc.yellow('没有需要完成的任务'));
575
+ return;
576
+ }
577
+
578
+ // 确认
579
+ if (!options.force) {
580
+ console.log('');
581
+ console.log(` 将完成 ${ids.length} 个任务:`);
582
+ for (const id of ids) {
583
+ console.log(` ${pc.dim(id)}`);
584
+ }
585
+ console.log('');
586
+ const confirmed = await p.confirm({
587
+ message: `确认完成这 ${ids.length} 个任务?`,
588
+ });
589
+ if (p.isCancel(confirmed) || !confirmed) {
590
+ p.outro('已取消');
591
+ return;
592
+ }
593
+ }
594
+
595
+ const s = p.spinner();
596
+ s.start(`正在完成 ${ids.length} 个任务...`);
597
+
598
+ try {
599
+ const result = await completeTasksInProject(projectId, ids);
600
+ s.stop('操作完成');
601
+ p.outro(
602
+ pc.green(`成功 ${result.completed.length} 个`) +
603
+ (result.failed.length > 0
604
+ ? pc.red(`,失败 ${result.failed.length} 个`)
605
+ : '')
606
+ );
607
+ } catch (err) {
608
+ s.stop('操作失败');
609
+ p.outro(pc.red((err as Error).message));
610
+ }
611
+ }
612
+
613
+ async function taskFindCommand(taskId: string): Promise<void> {
614
+ const s = p.spinner();
615
+ s.start('正在查找任务...');
616
+
617
+ try {
618
+ const task = await getTaskById(taskId);
619
+ s.stop('查找成功');
620
+ displayTaskDetail(task);
621
+ p.outro('任务详情如上');
622
+ } catch (err) {
623
+ s.stop('查找失败');
624
+ p.outro(pc.red((err as Error).message));
625
+ }
626
+ }
627
+
628
+ async function taskUndoneCommand(options: {
629
+ start?: string;
630
+ end?: string;
631
+ query?: string;
632
+ project?: string;
633
+ json?: boolean;
634
+ }): Promise<void> {
635
+ let tasks: Task[];
636
+
637
+ try {
638
+ if (options.query) {
639
+ // 预设查询
640
+ tasks = await listUndoneTasksByTimeQuery(options.query);
641
+ } else {
642
+ // 自定义日期范围
643
+ const params: { startDate?: string; endDate?: string; projectIds?: string[] } = {};
644
+ if (options.start) params.startDate = options.start;
645
+ if (options.end) params.endDate = options.end;
646
+ if (options.project) params.projectIds = [options.project];
647
+ tasks = await listUndoneTasksByDate(params);
648
+ }
649
+
650
+ if (options.json) {
651
+ console.log(JSON.stringify(tasks, null, 2));
652
+ return;
653
+ }
654
+
655
+ const s = p.spinner();
656
+ s.start('正在获取未完成任务...');
657
+ s.stop(`找到 ${tasks.length} 个未完成任务`);
658
+
659
+ if (tasks.length === 0) {
660
+ p.outro(pc.yellow('没有找到未完成任务'));
661
+ return;
662
+ }
663
+
664
+ console.log('');
665
+ displayTaskTable(tasks);
666
+ console.log('');
667
+ p.outro(`共 ${tasks.length} 个未完成任务`);
668
+ } catch (err) {
669
+ s.stop('获取失败');
670
+ p.outro(pc.red((err as Error).message));
671
+ }
672
+ }
673
+
674
+ async function taskSearchCommand(keyword: string): Promise<void> {
675
+ const s = p.spinner();
676
+ s.start(`正在搜索 "${keyword}"...`);
677
+
678
+ try {
679
+ const tasks = await searchTask(keyword);
680
+ s.stop(`找到 ${tasks.length} 个匹配任务`);
681
+
682
+ if (tasks.length === 0) {
683
+ p.outro(pc.yellow(`没有找到包含 "${keyword}" 的任务`));
684
+ return;
685
+ }
686
+
687
+ console.log('');
688
+ for (const task of tasks) {
689
+ const icon = statusIcon(task.status);
690
+ // 高亮关键词
691
+ const title = task.title.replace(
692
+ new RegExp(keyword, 'gi'),
693
+ (m) => pc.bold(pc.yellow(m))
694
+ );
695
+ console.log(` ${icon} ${title} ${pc.dim(task.id)} ${pc.dim(task.projectId)}`);
696
+ }
697
+ console.log('');
698
+ p.outro(`共 ${tasks.length} 个匹配`);
699
+ } catch (err) {
700
+ s.stop('搜索失败');
701
+ p.outro(pc.red((err as Error).message));
702
+ }
703
+ }
704
+
705
+ // ─── 注册命令 ────────────────────────────────────────
706
+
707
+ export function registerTaskCommands(cli: {
708
+ command: (name: string, desc: string) => {
709
+ option: (flag: string, desc: string) => {
710
+ action: (fn: (...args: unknown[]) => Promise<void>) => unknown;
711
+ };
712
+ action: (fn: (...args: unknown[]) => Promise<void>) => unknown;
713
+ };
714
+ }): void {
715
+ cli
716
+ .command('task-add [title]', '创建任务')
717
+ .option('-p, --project <id>', '项目 ID')
718
+ .option('--content <text>', '任务内容')
719
+ .option('--priority <n>', '优先级: 0(无) / 1(低) / 3(中) / 5(高)')
720
+ .option('--start-date <date>', '开始日期')
721
+ .option('--due-date <date>', '截止日期')
722
+ .option('--all-day', '全天任务')
723
+ .action(taskAddCommand);
724
+
725
+ cli
726
+ .command('task-get <projectId> <taskId>', '获取任务详情')
727
+ .action(taskGetCommand);
728
+
729
+ cli
730
+ .command('task-done <projectId> <taskId>', '完成任务')
731
+ .action(taskDoneCommand);
732
+
733
+ cli
734
+ .command('task-delete <projectId> <taskId>', '删除任务')
735
+ .action(taskDeleteCommand);
736
+
737
+ cli
738
+ .command('task-update <taskId>', '更新任务')
739
+ .option('-p, --project <id>', '项目 ID(必填)')
740
+ .option('--title <title>', '任务标题')
741
+ .option('--content <text>', '任务内容')
742
+ .option('--priority <n>', '优先级')
743
+ .option('--start-date <date>', '开始日期')
744
+ .option('--due-date <date>', '截止日期')
745
+ .action(taskUpdateCommand);
746
+
747
+ cli
748
+ .command('task-move <taskId>', '移动任务到其他项目')
749
+ .option('-f, --from <id>', '源项目 ID')
750
+ .option('-t, --to <id>', '目标项目 ID')
751
+ .action(taskMoveCommand);
752
+
753
+ cli
754
+ .command('task-completed', '查看已完成任务')
755
+ .option('-p, --project <id>', '按项目筛选')
756
+ .option('--start <date>', '开始日期')
757
+ .option('--end <date>', '结束日期')
758
+ .action(taskCompletedCommand);
759
+
760
+ cli
761
+ .command('task-list', '筛选任务')
762
+ .option('-p, --project <id>', '按项目筛选')
763
+ .option('--start <date>', '开始日期')
764
+ .option('--end <date>', '结束日期')
765
+ .option('--status <n>', '状态,逗号分隔: 0(待办),2(已完成)')
766
+ .option('--priority <n>', '优先级,逗号分隔: 0,1,3,5')
767
+ .option('--tag <tag>', '标签,逗号分隔')
768
+ .option('--json', '输出 JSON 格式')
769
+ .action(taskListCommand);
770
+
771
+ // ─── 批量 / 高级命令 ────────────────────────────
772
+
773
+ cli
774
+ .command('task-batch-add [jsonFile]', '批量创建任务')
775
+ .option('--stdin', '从标准输入读取 JSON')
776
+ .action(taskBatchAddCommand);
777
+
778
+ cli
779
+ .command('task-batch-update [jsonFile]', '批量更新任务')
780
+ .option('--stdin', '从标准输入读取 JSON')
781
+ .action(taskBatchUpdateCommand);
782
+
783
+ cli
784
+ .command('task-batch-done <projectId>', '批量完成任务')
785
+ .option('--task-ids <ids>', '任务 ID 列表,逗号分隔')
786
+ .option('--all', '完成项目下所有未完成任务')
787
+ .option('--force', '跳过确认')
788
+ .action(taskBatchDoneCommand);
789
+
790
+ cli
791
+ .command('task-find <taskId>', '按 ID 查找任务(无需项目 ID)')
792
+ .action(taskFindCommand);
793
+
794
+ cli
795
+ .command('task-undone', '查看未完成任务')
796
+ .option('--start <date>', '开始日期')
797
+ .option('--end <date>', '结束日期')
798
+ .option('--query <preset>', '预设查询: today|tomorrow|last24hour|next24hour|last7day|next7day')
799
+ .option('-p, --project <id>', '按项目筛选')
800
+ .option('--json', '输出 JSON 格式')
801
+ .action(taskUndoneCommand);
802
+
803
+ cli
804
+ .command('task-search <keyword>', '搜索任务')
805
+ .action(taskSearchCommand);
806
+ }