@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,303 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import type { Task, CreateProjectParams, UpdateProjectParams } from '../types.js';
4
+ import { formatTaskTime } from '../utils/format.js';
5
+ import {
6
+ getProjects,
7
+ getProject,
8
+ getProjectData,
9
+ createProject,
10
+ updateProject,
11
+ deleteProject,
12
+ } from '../api/resources.js';
13
+
14
+ // ─── 格式化工具 ──────────────────────────────────────
15
+
16
+ const PRIORITY_LABEL: Record<number, string> = {
17
+ 0: '无',
18
+ 1: '低',
19
+ 3: '中',
20
+ 5: '高',
21
+ };
22
+
23
+ function priorityText(priority?: number): string {
24
+ const v = priority ?? 0;
25
+ switch (v) {
26
+ case 5:
27
+ return pc.red('高');
28
+ case 3:
29
+ return pc.yellow('中');
30
+ case 1:
31
+ return pc.blue('低');
32
+ default:
33
+ return pc.dim('无');
34
+ }
35
+ }
36
+
37
+ function statusIcon(status?: number): string {
38
+ return status === 2 ? pc.green('✓') : '○';
39
+ }
40
+
41
+ function displayTaskList(tasks: Task[]): void {
42
+ if (tasks.length === 0) {
43
+ p.log.info(pc.dim(' (无任务)'));
44
+ return;
45
+ }
46
+
47
+ for (const task of tasks) {
48
+ const icon = statusIcon(task.status);
49
+ const title =
50
+ task.title.length > 30
51
+ ? task.title.slice(0, 30) + '…'
52
+ : task.title;
53
+ const time = formatTaskTime(task);
54
+ const timeStr = time ? pc.cyan(time) : '';
55
+ console.log(
56
+ ` ${icon} ${title} ${timeStr} ${priorityText(task.priority)}`
57
+ );
58
+ if (task.content) {
59
+ const content =
60
+ task.content.length > 50
61
+ ? task.content.slice(0, 50) + '…'
62
+ : task.content;
63
+ console.log(` ${pc.dim(content)}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ // ─── 命令实现 ────────────────────────────────────────
69
+
70
+ async function projectListCommand(): Promise<void> {
71
+ const s = p.spinner();
72
+ s.start('正在获取项目列表...');
73
+
74
+ try {
75
+ const projects = await getProjects();
76
+ s.stop(`找到 ${projects.length} 个项目`);
77
+
78
+ if (projects.length === 0) {
79
+ p.outro(pc.yellow('没有找到任何项目'));
80
+ return;
81
+ }
82
+
83
+ console.log('');
84
+ for (const project of projects) {
85
+ const status = project.closed ? pc.dim('已关闭') : pc.green('活跃');
86
+ console.log(
87
+ ` ${pc.bold(project.name)} ${pc.dim(project.id)}`
88
+ );
89
+ console.log(
90
+ ` 类型: ${project.kind || '-'} 视图: ${project.viewMode || '-'} ${status}`
91
+ );
92
+ }
93
+ console.log('');
94
+ p.outro(`共 ${projects.length} 个项目`);
95
+ } catch (err) {
96
+ s.stop('获取失败');
97
+ p.outro(pc.red((err as Error).message));
98
+ }
99
+ }
100
+
101
+ async function projectGetCommand(id: string): Promise<void> {
102
+ const s = p.spinner();
103
+ s.start('正在获取项目详情...');
104
+
105
+ try {
106
+ const project = await getProject(id);
107
+ s.stop('获取成功');
108
+
109
+ console.log('');
110
+ console.log(` ${pc.bold('名称')}: ${project.name}`);
111
+ console.log(` ${pc.bold('ID')}: ${project.id}`);
112
+ console.log(` ${pc.bold('颜色')}: ${project.color || '-'}`);
113
+ console.log(` ${pc.bold('类型')}: ${project.kind || '-'}`);
114
+ console.log(` ${pc.bold('视图')}: ${project.viewMode || '-'}`);
115
+ console.log(
116
+ ` ${pc.bold('状态')}: ${project.closed ? '已关闭' : '活跃'}`
117
+ );
118
+ if (project.groupId)
119
+ console.log(` ${pc.bold('分组')}: ${project.groupId}`);
120
+ if (project.permission)
121
+ console.log(` ${pc.bold('权限')}: ${project.permission}`);
122
+ console.log('');
123
+ } catch (err) {
124
+ s.stop('获取失败');
125
+ p.outro(pc.red((err as Error).message));
126
+ }
127
+ }
128
+
129
+ async function projectTasksCommand(
130
+ id: string,
131
+ options: { json?: boolean }
132
+ ): Promise<void> {
133
+ try {
134
+ const data = await getProjectData(id);
135
+
136
+ if (options.json) {
137
+ console.log(JSON.stringify(data.tasks, null, 2));
138
+ return;
139
+ }
140
+
141
+ const s = p.spinner();
142
+ s.start('正在获取项目任务...');
143
+ s.stop(
144
+ `项目「${data.project.name}」下有 ${data.tasks.length} 个任务`
145
+ );
146
+
147
+ if (data.tasks.length === 0) {
148
+ p.outro(pc.yellow('该项目下没有任务'));
149
+ return;
150
+ }
151
+
152
+ console.log('');
153
+ displayTaskList(data.tasks);
154
+ console.log('');
155
+ p.outro(`共 ${data.tasks.length} 个任务`);
156
+ } catch (err) {
157
+ s.stop('获取失败');
158
+ p.outro(pc.red((err as Error).message));
159
+ }
160
+ }
161
+
162
+ async function projectCreateCommand(
163
+ name: string | undefined,
164
+ options: {
165
+ color?: string;
166
+ viewMode?: string;
167
+ kind?: string;
168
+ }
169
+ ): Promise<void> {
170
+ if (!name) {
171
+ const input = await p.text({ message: '请输入项目名称' });
172
+ if (p.isCancel(input)) {
173
+ p.outro('已取消');
174
+ return;
175
+ }
176
+ name = input;
177
+ }
178
+
179
+ const params: CreateProjectParams = { name };
180
+ if (options.color) params.color = options.color;
181
+ if (options.viewMode)
182
+ params.viewMode = options.viewMode as CreateProjectParams['viewMode'];
183
+ if (options.kind)
184
+ params.kind = options.kind as CreateProjectParams['kind'];
185
+
186
+ const s = p.spinner();
187
+ s.start('正在创建项目...');
188
+
189
+ try {
190
+ const project = await createProject(params);
191
+ s.stop('创建成功');
192
+ p.outro(
193
+ pc.green(`项目「${project.name}」已创建 (ID: ${project.id})`)
194
+ );
195
+ } catch (err) {
196
+ s.stop('创建失败');
197
+ p.outro(pc.red((err as Error).message));
198
+ }
199
+ }
200
+
201
+ async function projectUpdateCommand(
202
+ id: string,
203
+ options: {
204
+ name?: string;
205
+ color?: string;
206
+ viewMode?: string;
207
+ kind?: string;
208
+ }
209
+ ): Promise<void> {
210
+ const params: UpdateProjectParams = {};
211
+ if (options.name) params.name = options.name;
212
+ if (options.color) params.color = options.color;
213
+ if (options.viewMode)
214
+ params.viewMode = options.viewMode as UpdateProjectParams['viewMode'];
215
+ if (options.kind)
216
+ params.kind = options.kind as UpdateProjectParams['kind'];
217
+
218
+ if (Object.keys(params).length === 0) {
219
+ p.outro(
220
+ pc.yellow(
221
+ '未指定任何更新内容,使用 --name / --color / --view-mode / --kind 选项'
222
+ )
223
+ );
224
+ return;
225
+ }
226
+
227
+ const s = p.spinner();
228
+ s.start('正在更新项目...');
229
+
230
+ try {
231
+ const project = await updateProject(id, params);
232
+ s.stop('更新成功');
233
+ p.outro(pc.green(`项目「${project.name}」已更新`));
234
+ } catch (err) {
235
+ s.stop('更新失败');
236
+ p.outro(pc.red((err as Error).message));
237
+ }
238
+ }
239
+
240
+ async function projectDeleteCommand(id: string): Promise<void> {
241
+ const confirmed = await p.confirm({
242
+ message: `确认删除项目 ${pc.red(id)}?此操作不可撤销`,
243
+ });
244
+ if (p.isCancel(confirmed) || !confirmed) {
245
+ p.outro('已取消');
246
+ return;
247
+ }
248
+
249
+ const s = p.spinner();
250
+ s.start('正在删除项目...');
251
+
252
+ try {
253
+ await deleteProject(id);
254
+ s.stop('删除成功');
255
+ p.outro(pc.green('项目已删除'));
256
+ } catch (err) {
257
+ s.stop('删除失败');
258
+ p.outro(pc.red((err as Error).message));
259
+ }
260
+ }
261
+
262
+ // ─── 注册命令 ────────────────────────────────────────
263
+
264
+ export function registerProjectCommands(cli: {
265
+ command: (name: string, desc: string) => {
266
+ option: (flag: string, desc: string) => {
267
+ action: (fn: (...args: unknown[]) => Promise<void>) => unknown;
268
+ };
269
+ action: (fn: (...args: unknown[]) => Promise<void>) => unknown;
270
+ };
271
+ }): void {
272
+ cli
273
+ .command('project-list', '列出所有项目')
274
+ .action(projectListCommand);
275
+
276
+ cli
277
+ .command('project-get <id>', '获取项目详情')
278
+ .action(projectGetCommand);
279
+
280
+ cli
281
+ .command('project-tasks <id>', '获取项目下的任务')
282
+ .option('--json', '输出 JSON 格式')
283
+ .action(projectTasksCommand);
284
+
285
+ cli
286
+ .command('project-create [name]', '创建项目')
287
+ .option('--color <color>', '项目颜色,如 #F18181')
288
+ .option('--view-mode <mode>', '视图模式: list / kanban / timeline')
289
+ .option('--kind <kind>', '项目类型: TASK / NOTE')
290
+ .action(projectCreateCommand);
291
+
292
+ cli
293
+ .command('project-update <id>', '更新项目')
294
+ .option('--name <name>', '项目名称')
295
+ .option('--color <color>', '项目颜色')
296
+ .option('--view-mode <mode>', '视图模式')
297
+ .option('--kind <kind>', '项目类型')
298
+ .action(projectUpdateCommand);
299
+
300
+ cli
301
+ .command('project-delete <id>', '删除项目')
302
+ .action(projectDeleteCommand);
303
+ }