shawnxixi-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/.env.example +2 -0
- package/.github/workflows/ci.yml +48 -0
- package/AUTH-RESEARCH.md +230 -0
- package/CLAUDE.md +102 -0
- package/NPM-PUBLISH-RESEARCH.md +163 -0
- package/README.md +136 -0
- package/dist/commands/biz/calendar.d.ts +12 -0
- package/dist/commands/biz/calendar.d.ts.map +1 -0
- package/dist/commands/biz/calendar.js +35 -0
- package/dist/commands/biz/calendar.js.map +1 -0
- package/dist/commands/biz/finance.d.ts +12 -0
- package/dist/commands/biz/finance.d.ts.map +1 -0
- package/dist/commands/biz/finance.js +34 -0
- package/dist/commands/biz/finance.js.map +1 -0
- package/dist/commands/biz/item.d.ts +12 -0
- package/dist/commands/biz/item.d.ts.map +1 -0
- package/dist/commands/biz/item.js +32 -0
- package/dist/commands/biz/item.js.map +1 -0
- package/dist/commands/biz/record.d.ts +28 -0
- package/dist/commands/biz/record.d.ts.map +1 -0
- package/dist/commands/biz/record.js +34 -0
- package/dist/commands/biz/record.js.map +1 -0
- package/dist/commands/biz/todo.d.ts +35 -0
- package/dist/commands/biz/todo.d.ts.map +1 -0
- package/dist/commands/biz/todo.js +73 -0
- package/dist/commands/biz/todo.js.map +1 -0
- package/dist/commands/sys/health.d.ts +9 -0
- package/dist/commands/sys/health.d.ts.map +1 -0
- package/dist/commands/sys/health.js +28 -0
- package/dist/commands/sys/health.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +199 -0
- package/dist/index.js.map +1 -0
- package/dist/services/api.d.ts +37 -0
- package/dist/services/api.d.ts.map +1 -0
- package/dist/services/api.js +218 -0
- package/dist/services/api.js.map +1 -0
- package/jest.config.js +11 -0
- package/package.json +40 -0
- package/src/commands/biz/calendar.ts +31 -0
- package/src/commands/biz/finance.ts +30 -0
- package/src/commands/biz/item.ts +28 -0
- package/src/commands/biz/record.ts +38 -0
- package/src/commands/biz/todo.ts +74 -0
- package/src/commands/sys/health.ts +23 -0
- package/src/index.ts +209 -0
- package/src/services/api.ts +226 -0
- package/tests/commands/todo.test.ts +39 -0
- package/tests/services/api.test.ts +39 -0
- package/tsconfig.json +20 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* shawnxixi-cli
|
|
5
|
+
* 肖嘻 CLI 工具入口
|
|
6
|
+
* 命令命名空间:biz:*(日常业务)、sys:*(系统操作)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { todoCommands } from './commands/biz/todo';
|
|
10
|
+
import { recordCommands } from './commands/biz/record';
|
|
11
|
+
import { financeCommands } from './commands/biz/finance';
|
|
12
|
+
import { itemCommands } from './commands/biz/item';
|
|
13
|
+
import { calendarCommands } from './commands/biz/calendar';
|
|
14
|
+
import { syncHealth } from './commands/sys/health';
|
|
15
|
+
|
|
16
|
+
type AsyncCommandHandler = (...args: any[]) => Promise<void>;
|
|
17
|
+
|
|
18
|
+
interface CommandMap {
|
|
19
|
+
[key: string]: AsyncCommandHandler;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const commands: CommandMap = {
|
|
23
|
+
// Todo
|
|
24
|
+
'biz:todo:create': async (title: string, args: string[]) => {
|
|
25
|
+
const options = parseTodoOptions(args);
|
|
26
|
+
await todoCommands.create(title, options);
|
|
27
|
+
},
|
|
28
|
+
'biz:todo:list': async () => {
|
|
29
|
+
await todoCommands.list({});
|
|
30
|
+
},
|
|
31
|
+
'biz:todo:done': async (id: string) => {
|
|
32
|
+
await todoCommands.done(parseInt(id, 10));
|
|
33
|
+
},
|
|
34
|
+
'biz:todo:delete': async (id: string) => {
|
|
35
|
+
await todoCommands.delete(parseInt(id, 10));
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Record
|
|
39
|
+
'biz:record:create': async (title: string, args: string[]) => {
|
|
40
|
+
const options = parseRecordOptions(args);
|
|
41
|
+
await recordCommands.create(title, options.content || '', options);
|
|
42
|
+
},
|
|
43
|
+
'biz:record:list': async () => {
|
|
44
|
+
await recordCommands.list({});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Finance
|
|
48
|
+
'biz:finance:create': async (amount: string, args: string[]) => {
|
|
49
|
+
const options = parseFinanceOptions(args);
|
|
50
|
+
await financeCommands.create(amount, options);
|
|
51
|
+
},
|
|
52
|
+
'biz:finance:list': async (args: string[]) => {
|
|
53
|
+
const options = parseFinanceOptions(args);
|
|
54
|
+
await financeCommands.list(options);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Item
|
|
58
|
+
'biz:item:create': async (name: string, args: string[]) => {
|
|
59
|
+
const options = parseItemOptions(args);
|
|
60
|
+
await itemCommands.create(name, options);
|
|
61
|
+
},
|
|
62
|
+
'biz:item:list': async () => {
|
|
63
|
+
await itemCommands.list({});
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Calendar
|
|
67
|
+
'biz:calendar:create': async (title: string, args: string[]) => {
|
|
68
|
+
const options = parseCalendarOptions(args);
|
|
69
|
+
await calendarCommands.create(title, options);
|
|
70
|
+
},
|
|
71
|
+
'biz:calendar:list': async () => {
|
|
72
|
+
await calendarCommands.list({});
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// System
|
|
76
|
+
'sys:health:sync': async () => {
|
|
77
|
+
await syncHealth();
|
|
78
|
+
},
|
|
79
|
+
'sys:health:query': async (dataType: string, args: string[]) => {
|
|
80
|
+
const options = parseHealthOptions(args);
|
|
81
|
+
await syncHealth(dataType, options);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ============ Option Parsers ============
|
|
86
|
+
|
|
87
|
+
function parseTodoOptions(args: string[]): { due?: string; priority?: string } {
|
|
88
|
+
const options: { due?: string; priority?: string } = {};
|
|
89
|
+
for (let i = 0; i < args.length; i++) {
|
|
90
|
+
if (args[i] === '--due' && args[i + 1]) options.due = args[++i];
|
|
91
|
+
if (args[i] === '--priority' && args[i + 1]) options.priority = args[++i];
|
|
92
|
+
}
|
|
93
|
+
return options;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseRecordOptions(args: string[]): { content?: string; type?: string; tags?: string } {
|
|
97
|
+
const options: { content?: string; type?: string; tags?: string } = {};
|
|
98
|
+
for (let i = 0; i < args.length; i++) {
|
|
99
|
+
if (args[i] === '--content' && args[i + 1]) options.content = args[++i];
|
|
100
|
+
if (args[i] === '--type' && args[i + 1]) options.type = args[++i];
|
|
101
|
+
if (args[i] === '--tags' && args[i + 1]) options.tags = args[++i];
|
|
102
|
+
}
|
|
103
|
+
return options;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseFinanceOptions(args: string[]): { type?: string; category?: string; desc?: string } {
|
|
107
|
+
const options: { type?: string; category?: string; desc?: string } = {};
|
|
108
|
+
for (let i = 0; i < args.length; i++) {
|
|
109
|
+
if (args[i] === '--type' && args[i + 1]) options.type = args[++i];
|
|
110
|
+
if (args[i] === '--category' && args[i + 1]) options.category = args[++i];
|
|
111
|
+
if (args[i] === '--desc' && args[i + 1]) options.desc = args[++i];
|
|
112
|
+
}
|
|
113
|
+
return options;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseItemOptions(args: string[]): { location?: string; expire?: string; category?: string } {
|
|
117
|
+
const options: { location?: string; expire?: string; category?: string } = {};
|
|
118
|
+
for (let i = 0; i < args.length; i++) {
|
|
119
|
+
if (args[i] === '--location' && args[i + 1]) options.location = args[++i];
|
|
120
|
+
if (args[i] === '--expire' && args[i + 1]) options.expire = args[++i];
|
|
121
|
+
if (args[i] === '--category' && args[i + 1]) options.category = args[++i];
|
|
122
|
+
}
|
|
123
|
+
return options;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseCalendarOptions(args: string[]): { start?: string; end?: string; desc?: string } {
|
|
127
|
+
const options: { start?: string; end?: string; desc?: string } = {};
|
|
128
|
+
for (let i = 0; i < args.length; i++) {
|
|
129
|
+
if (args[i] === '--start' && args[i + 1]) options.start = args[++i];
|
|
130
|
+
if (args[i] === '--end' && args[i + 1]) options.end = args[++i];
|
|
131
|
+
if (args[i] === '--desc' && args[i + 1]) options.desc = args[++i];
|
|
132
|
+
}
|
|
133
|
+
return options;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseHealthOptions(args: string[]): { startDate?: string; endDate?: string } {
|
|
137
|
+
const options: { startDate?: string; endDate?: string } = {};
|
|
138
|
+
for (let i = 0; i < args.length; i++) {
|
|
139
|
+
if (args[i] === '--from' && args[i + 1]) options.startDate = args[++i];
|
|
140
|
+
if (args[i] === '--to' && args[i + 1]) options.endDate = args[++i];
|
|
141
|
+
}
|
|
142
|
+
return options;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============ CLI Entry ============
|
|
146
|
+
|
|
147
|
+
function printUsage(): void {
|
|
148
|
+
console.log(`
|
|
149
|
+
shawnxixi CLI - 肖嘻 CLI 工具
|
|
150
|
+
|
|
151
|
+
Usage: shawnxixi <command> [args...]
|
|
152
|
+
|
|
153
|
+
Business Commands (biz:*):
|
|
154
|
+
biz todo create <title> [--due YYYY-MM-DD] [--priority low|medium|high]
|
|
155
|
+
biz todo list
|
|
156
|
+
biz todo done <id>
|
|
157
|
+
biz todo delete <id>
|
|
158
|
+
biz record create <title> [--content TEXT] [--type diary|pet|note|idea] [--tags TAG1,TAG2]
|
|
159
|
+
biz record list
|
|
160
|
+
biz finance create <amount> [--type income|expense] [--category CAT] [--desc TEXT]
|
|
161
|
+
biz finance list [--type income|expense] [--category CAT]
|
|
162
|
+
biz item create <name> [--location LOC] [--expire YYYY-MM-DD] [--category CAT]
|
|
163
|
+
biz item list
|
|
164
|
+
biz calendar create <title> --start DATETIME --end DATETIME [--desc TEXT]
|
|
165
|
+
biz calendar list
|
|
166
|
+
|
|
167
|
+
System Commands (sys:*):
|
|
168
|
+
sys health sync [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
169
|
+
sys health query <dataType> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
shawnxixi biz todo create "开会" --due 2026-04-12 --priority high
|
|
173
|
+
shawnxixi biz finance create 50 --category food --desc "午饭"
|
|
174
|
+
shawnxixi biz calendar create "面试" --start "2026-04-15 14:00" --end "2026-04-15 15:00"
|
|
175
|
+
`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function main(): void {
|
|
179
|
+
const args = process.argv.slice(2);
|
|
180
|
+
|
|
181
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
182
|
+
printUsage();
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle special case: "biz todo list" -> "biz:todo:list"
|
|
187
|
+
let commandKey: string;
|
|
188
|
+
if (args[0] === 'biz' || args[0] === 'sys') {
|
|
189
|
+
commandKey = args.join(':');
|
|
190
|
+
} else {
|
|
191
|
+
commandKey = args[0];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handler = commands[commandKey];
|
|
195
|
+
|
|
196
|
+
if (!handler) {
|
|
197
|
+
console.error(`Unknown command: ${commandKey}`);
|
|
198
|
+
printUsage();
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const handlerArgs = args.slice(1);
|
|
203
|
+
handler(...handlerArgs).catch((error) => {
|
|
204
|
+
console.error('Command failed:', error);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
main();
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Service - HTTP 调用后端
|
|
3
|
+
* 封装与 shawnxixi-server 的通信
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios, { AxiosInstance } from 'axios';
|
|
7
|
+
|
|
8
|
+
const API_BASE = process.env.SHAWNXIXI_API_URL || 'http://localhost:8080';
|
|
9
|
+
|
|
10
|
+
class ApiService {
|
|
11
|
+
private client: AxiosInstance;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.client = axios.create({
|
|
15
|
+
baseURL: API_BASE,
|
|
16
|
+
timeout: 10000,
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 请求拦截器:添加 auth token
|
|
23
|
+
this.client.interceptors.request.use(async (config) => {
|
|
24
|
+
const token = await this.getToken();
|
|
25
|
+
if (token) {
|
|
26
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
27
|
+
}
|
|
28
|
+
return config;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 响应拦截器:处理 401 刷新 token
|
|
32
|
+
this.client.interceptors.response.use(
|
|
33
|
+
(response) => response,
|
|
34
|
+
async (error) => {
|
|
35
|
+
if (error.response?.status === 401) {
|
|
36
|
+
// token 过期,尝试刷新
|
|
37
|
+
await this.refreshToken();
|
|
38
|
+
// 重试原请求
|
|
39
|
+
const config = error.config;
|
|
40
|
+
const token = await this.getToken();
|
|
41
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
42
|
+
return this.client(config);
|
|
43
|
+
}
|
|
44
|
+
return Promise.reject(error);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async getToken(): Promise<string | null> {
|
|
50
|
+
// TODO: 从 Keychain 读取 token
|
|
51
|
+
return process.env.SHAWNXIXI_API_TOKEN || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async refreshToken(): Promise<void> {
|
|
55
|
+
// TODO: 实现 token 刷新逻辑
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============ Todo API ============
|
|
59
|
+
async createTodo(title: string, dueDate?: string, priority?: string) {
|
|
60
|
+
const { data } = await this.client.post('/api/v1/todos', {
|
|
61
|
+
title,
|
|
62
|
+
dueDate,
|
|
63
|
+
priority: priority || 'medium',
|
|
64
|
+
status: 'pending',
|
|
65
|
+
});
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async listTodos(status?: string) {
|
|
70
|
+
const params = status ? { status } : {};
|
|
71
|
+
const { data } = await this.client.get('/api/v1/todos', { params });
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async updateTodo(id: number, updates: Partial<{title: string; status: string; priority: string; dueDate: string}>) {
|
|
76
|
+
const { data } = await this.client.put(`/api/v1/todos/${id}`, updates);
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async deleteTodo(id: number) {
|
|
81
|
+
const { data } = await this.client.delete(`/api/v1/todos/${id}`);
|
|
82
|
+
return data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============ Record API ============
|
|
86
|
+
async createRecord(title: string, content: string, type: string = 'diary', tags?: string) {
|
|
87
|
+
const { data } = await this.client.post('/api/v1/records', {
|
|
88
|
+
title,
|
|
89
|
+
content,
|
|
90
|
+
type,
|
|
91
|
+
tags,
|
|
92
|
+
});
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listRecords(type?: string, tags?: string) {
|
|
97
|
+
const params: Record<string, string> = {};
|
|
98
|
+
if (type) params.type = type;
|
|
99
|
+
if (tags) params.tags = tags;
|
|
100
|
+
const { data } = await this.client.get('/api/v1/records', { params });
|
|
101
|
+
return data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============ Finance API ============
|
|
105
|
+
async createFinance(amount: number, type: string, category: string, description?: string) {
|
|
106
|
+
const { data } = await this.client.post('/api/v1/finances', {
|
|
107
|
+
amount,
|
|
108
|
+
type,
|
|
109
|
+
category,
|
|
110
|
+
description,
|
|
111
|
+
});
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async listFinances(type?: string, category?: string) {
|
|
116
|
+
const params: Record<string, string> = {};
|
|
117
|
+
if (type) params.type = type;
|
|
118
|
+
if (category) params.category = category;
|
|
119
|
+
const { data } = await this.client.get('/api/v1/finances', { params });
|
|
120
|
+
return data;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============ Health API ============
|
|
124
|
+
async syncHealthData() {
|
|
125
|
+
const { data } = await this.client.post('/api/v1/health-data/sync');
|
|
126
|
+
return data;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async queryHealthData(dataType?: string, startDate?: string, endDate?: string) {
|
|
130
|
+
const params: Record<string, string> = {};
|
|
131
|
+
if (dataType) params.dataType = dataType;
|
|
132
|
+
if (startDate) params.startDate = startDate;
|
|
133
|
+
if (endDate) params.endDate = endDate;
|
|
134
|
+
const { data } = await this.client.get('/api/v1/health-data', { params });
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============ Item API ============
|
|
139
|
+
async createItem(name: string, category?: string, location?: string, expireDate?: string) {
|
|
140
|
+
const { data } = await this.client.post('/api/v1/items', {
|
|
141
|
+
name,
|
|
142
|
+
category,
|
|
143
|
+
location,
|
|
144
|
+
expireDate,
|
|
145
|
+
});
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async listItems(category?: string, status?: string) {
|
|
150
|
+
const params: Record<string, string> = {};
|
|
151
|
+
if (category) params.category = category;
|
|
152
|
+
if (status) params.status = status;
|
|
153
|
+
const { data } = await this.client.get('/api/v1/items', { params });
|
|
154
|
+
return data;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============ Calendar API ============
|
|
158
|
+
async createCalendarEvent(title: string, startTime: string, endTime: string, description?: string, location?: string) {
|
|
159
|
+
const { data } = await this.client.post('/api/v1/calendar-events', {
|
|
160
|
+
title,
|
|
161
|
+
startTime,
|
|
162
|
+
endTime,
|
|
163
|
+
description,
|
|
164
|
+
location,
|
|
165
|
+
});
|
|
166
|
+
return data;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async listCalendarEvents(startTime?: string, endTime?: string) {
|
|
170
|
+
const params: Record<string, string> = {};
|
|
171
|
+
if (startTime) params.startTime = startTime;
|
|
172
|
+
if (endTime) params.endTime = endTime;
|
|
173
|
+
const { data } = await this.client.get('/api/v1/calendar-events', { params });
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============ Reminder API ============
|
|
178
|
+
async createReminder(title: string, triggerTime: string, type: string = 'once') {
|
|
179
|
+
const { data } = await this.client.post('/api/v1/reminders', {
|
|
180
|
+
title,
|
|
181
|
+
triggerTime,
|
|
182
|
+
type,
|
|
183
|
+
status: 'active',
|
|
184
|
+
});
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async listReminders(status?: string, type?: string) {
|
|
189
|
+
const params: Record<string, string> = {};
|
|
190
|
+
if (status) params.status = status;
|
|
191
|
+
if (type) params.type = type;
|
|
192
|
+
const { data } = await this.client.get('/api/v1/reminders', { params });
|
|
193
|
+
return data;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============ AuditLog API ============
|
|
197
|
+
async listAuditLogs(entityType?: string, entityId?: number, operation?: string) {
|
|
198
|
+
const params: Record<string, string> = {};
|
|
199
|
+
if (entityType) params.entityType = entityType;
|
|
200
|
+
if (entityId) params.entityId = String(entityId);
|
|
201
|
+
if (operation) params.operation = operation;
|
|
202
|
+
const { data } = await this.client.get('/api/v1/audit-logs', { params });
|
|
203
|
+
return data;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============ Task API ============
|
|
207
|
+
async createTask(title: string, description?: string, priority?: string, source?: string) {
|
|
208
|
+
const { data } = await this.client.post('/api/v1/tasks', {
|
|
209
|
+
title,
|
|
210
|
+
description,
|
|
211
|
+
priority: priority || 'medium',
|
|
212
|
+
source: source || 'cli',
|
|
213
|
+
status: 'pending',
|
|
214
|
+
});
|
|
215
|
+
return data;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async listTasks(status?: string) {
|
|
219
|
+
const params: Record<string, string> = {};
|
|
220
|
+
if (status) params.status = status;
|
|
221
|
+
const { data } = await this.client.get('/api/v1/tasks', { params });
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const apiService = new ApiService();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo Command Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { todoCommands } from '../../src/commands/biz/todo';
|
|
6
|
+
|
|
7
|
+
// Mock apiService
|
|
8
|
+
jest.mock('../../src/services/api', () => ({
|
|
9
|
+
apiService: {
|
|
10
|
+
createTodo: jest.fn().mockResolvedValue({ id: 1, title: '测试待办' }),
|
|
11
|
+
listTodos: jest.fn().mockResolvedValue([{ id: 1, title: '测试待办' }]),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { apiService } from '../../src/services/api';
|
|
16
|
+
|
|
17
|
+
describe('biz todo commands', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('create', () => {
|
|
23
|
+
it('should call apiService.createTodo', async () => {
|
|
24
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
25
|
+
await todoCommands.create('测试待办');
|
|
26
|
+
expect(apiService.createTodo).toHaveBeenCalledWith('测试待办', undefined, undefined);
|
|
27
|
+
consoleSpy.mockRestore();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('list', () => {
|
|
32
|
+
it('should call apiService.listTodos', async () => {
|
|
33
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
34
|
+
await todoCommands.list();
|
|
35
|
+
expect(apiService.listTodos).toHaveBeenCalledWith(undefined);
|
|
36
|
+
consoleSpy.mockRestore();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Service Tests with Mock
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { apiService } from '../../src/services/api';
|
|
6
|
+
|
|
7
|
+
// Mock axios
|
|
8
|
+
jest.mock('axios', () => ({
|
|
9
|
+
create: () => ({
|
|
10
|
+
interceptors: {
|
|
11
|
+
request: { use: jest.fn((cb) => cb) },
|
|
12
|
+
response: { use: jest.fn((successCb, _errorCb) => successCb) },
|
|
13
|
+
},
|
|
14
|
+
get: jest.fn().mockResolvedValue({ data: {} }),
|
|
15
|
+
post: jest.fn().mockResolvedValue({ data: { id: 1 } }),
|
|
16
|
+
put: jest.fn().mockResolvedValue({ data: {} }),
|
|
17
|
+
delete: jest.fn().mockResolvedValue({ data: {} }),
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('ApiService', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('createTodo', () => {
|
|
27
|
+
it('should call POST /api/v1/todos', async () => {
|
|
28
|
+
const result = await apiService.createTodo('测试待办');
|
|
29
|
+
expect(result).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('listTodos', () => {
|
|
34
|
+
it('should call GET /api/v1/todos', async () => {
|
|
35
|
+
const result = await apiService.listTodos();
|
|
36
|
+
expect(result).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
20
|
+
}
|