@cloudglab/yapi-cli 0.0.7 → 0.0.9
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/CHANGELOG.md +25 -0
- package/README.md +31 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +43 -5
- package/dist/cli.js.map +1 -1
- package/dist/core/changelog.js +7 -14
- package/dist/core/changelog.js.map +1 -1
- package/dist/core/cli-output.js +7 -6
- package/dist/core/cli-output.js.map +1 -1
- package/dist/core/cli-registry.js +61 -8
- package/dist/core/cli-registry.js.map +1 -1
- package/dist/core/manifest.js +14 -4
- package/dist/core/manifest.js.map +1 -1
- package/dist/core/output.d.ts +2 -2
- package/dist/core/output.js +12 -45
- package/dist/core/output.js.map +1 -1
- package/dist/core/tool-registry.js +5 -0
- package/dist/core/tool-registry.js.map +1 -1
- package/dist/core/update-probe.d.ts +3 -1
- package/dist/core/update-probe.js +9 -4
- package/dist/core/update-probe.js.map +1 -1
- package/dist/core/url-parser.js +10 -0
- package/dist/core/url-parser.js.map +1 -1
- package/dist/install.d.ts +9 -1
- package/dist/install.js +74 -20
- package/dist/install.js.map +1 -1
- package/dist/manifest.json +42 -1
- package/dist/services/yapi/api.d.ts +255 -79
- package/dist/services/yapi/api.js +162 -58
- package/dist/services/yapi/api.js.map +1 -1
- package/dist/services/yapi/auth.d.ts +8 -3
- package/dist/services/yapi/auth.js +10 -6
- package/dist/services/yapi/auth.js.map +1 -1
- package/dist/services/yapi/authCache.js +9 -2
- package/dist/services/yapi/authCache.js.map +1 -1
- package/dist/services/yapi/config.d.ts +6 -0
- package/dist/services/yapi/config.js +85 -8
- package/dist/services/yapi/config.js.map +1 -1
- package/dist/services/yapi/index.d.ts +3 -3
- package/dist/services/yapi/index.js +2 -3
- package/dist/services/yapi/index.js.map +1 -1
- package/dist/services/yapi/types.d.ts +38 -0
- package/dist/tools/shared.d.ts +17 -1
- package/dist/tools/shared.js +25 -6
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/yapi/docs-sync.js +3 -0
- package/dist/tools/yapi/docs-sync.js.map +1 -1
- package/dist/tools/yapi/groups.d.ts +1 -1
- package/dist/tools/yapi/groups.js +1 -1
- package/dist/tools/yapi/groups.js.map +1 -1
- package/dist/tools/yapi/register-auth.js +116 -104
- package/dist/tools/yapi/register-auth.js.map +1 -1
- package/dist/tools/yapi/register-group.js +118 -93
- package/dist/tools/yapi/register-group.js.map +1 -1
- package/dist/tools/yapi/register-interface.js +433 -199
- package/dist/tools/yapi/register-interface.js.map +1 -1
- package/dist/tools/yapi/register-mock.d.ts +2 -0
- package/dist/tools/yapi/register-mock.js +240 -0
- package/dist/tools/yapi/register-mock.js.map +1 -0
- package/dist/tools/yapi/register-project.js +344 -223
- package/dist/tools/yapi/register-project.js.map +1 -1
- package/dist/tools/yapi/register-test.js +33 -25
- package/dist/tools/yapi/register-test.js.map +1 -1
- package/dist/tools/yapi/register-util.js +444 -350
- package/dist/tools/yapi/register-util.js.map +1 -1
- package/dist/tools/yapi/register.js +3 -0
- package/dist/tools/yapi/register.js.map +1 -1
- package/dist/tools/yapi/utils.d.ts +49 -0
- package/dist/tools/yapi/utils.js +124 -2
- package/dist/tools/yapi/utils.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +18 -5
- package/skills/yapi-cli/SKILL.md +37 -12
- package/skills/yapi-cli/reference/auth.md +34 -45
- package/skills/yapi-cli/reference/cheatsheet.md +156 -0
- package/skills/yapi-cli/reference/cli.md +43 -39
- package/skills/yapi-cli/reference/commands.md +35 -124
- package/skills/yapi-cli/reference/group.md +46 -19
- package/skills/yapi-cli/reference/index.md +32 -0
- package/skills/yapi-cli/reference/install.md +30 -6
- package/skills/yapi-cli/reference/interface.md +71 -145
- package/skills/yapi-cli/reference/mock.md +93 -0
- package/skills/yapi-cli/reference/overview.md +7 -5
- package/skills/yapi-cli/reference/project.md +67 -87
- package/skills/yapi-cli/reference/scenarios.md +184 -0
- package/skills/yapi-cli/reference/test.md +20 -17
- package/skills/yapi-cli/reference/tooling.md +89 -0
- package/dist/services/yapi/cache.d.ts +0 -27
- package/dist/services/yapi/cache.js +0 -88
- package/dist/services/yapi/cache.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CliApiError, CliValidationError } from '../../core/errors.js';
|
|
3
|
-
import { jsonResult, paginationSchema, optionalIdFilter, projectIdSchema } from '../shared.js';
|
|
3
|
+
import { confirmSchema, jsonResult, paginationSchema, optionalIdFilter, projectIdSchema, runWithPreview } from '../shared.js';
|
|
4
|
+
import { previewOrAssertWriteAllowed } from '../../core/write-guard.js';
|
|
4
5
|
import { createYApiServices } from './utils.js';
|
|
5
6
|
export function registerProjectCommands(registry) {
|
|
6
7
|
// ==================== 项目列表 ====================
|
|
@@ -44,6 +45,8 @@ export function registerProjectCommands(registry) {
|
|
|
44
45
|
token: p.token,
|
|
45
46
|
env: p.env,
|
|
46
47
|
members: p.members?.length ?? 0,
|
|
48
|
+
isMockOpen: p.is_mock_open,
|
|
49
|
+
projectMockScript: p.project_mock_script,
|
|
47
50
|
},
|
|
48
51
|
});
|
|
49
52
|
}, '查看项目详情', { costHint: 'low', nextBestTools: ['interface-list', 'project-token', 'env'] });
|
|
@@ -60,13 +63,11 @@ export function registerProjectCommands(registry) {
|
|
|
60
63
|
}, '获取项目 Token', { costHint: 'low', nextBestTools: ['project-get', 'interface-list'] });
|
|
61
64
|
// ==================== 项目环境列表 ====================
|
|
62
65
|
registry.tool('env', {
|
|
63
|
-
projectId: z.
|
|
66
|
+
projectId: z.number().int().positive().describe('项目 ID,必填。YApi 服务端分配的内部数字 ID,用于查询该项目下的环境列表。'),
|
|
64
67
|
}, async (input) => {
|
|
65
68
|
const { api } = createYApiServices();
|
|
66
69
|
const result = await api.listProjectEnvs(input.projectId);
|
|
67
|
-
const envs = result.data
|
|
68
|
-
? (Array.isArray(result.data) ? result.data : result.data.env ?? [])
|
|
69
|
-
: [];
|
|
70
|
+
const envs = result.data ? (Array.isArray(result.data.env) ? result.data.env : []) : [];
|
|
70
71
|
return jsonResult({
|
|
71
72
|
projectId: input.projectId,
|
|
72
73
|
envs: envs.map((e) => ({
|
|
@@ -78,7 +79,7 @@ export function registerProjectCommands(registry) {
|
|
|
78
79
|
}, '列出项目环境', { costHint: 'low', nextBestTools: ['project-get', 'member', 'interface-list'] });
|
|
79
80
|
// ==================== 成员列表 ====================
|
|
80
81
|
registry.tool('member', {
|
|
81
|
-
projectId: z.
|
|
82
|
+
projectId: z.number().int().positive().describe('项目 ID,必填。返回该项目的成员 uid、username、role、email 等信息。'),
|
|
82
83
|
}, async (input) => {
|
|
83
84
|
const { api } = createYApiServices();
|
|
84
85
|
const result = await api.listProjectMembers(input.projectId);
|
|
@@ -97,9 +98,7 @@ export function registerProjectCommands(registry) {
|
|
|
97
98
|
registry.tool('follow', {}, async () => {
|
|
98
99
|
const { api } = createYApiServices();
|
|
99
100
|
const result = await api.listFollowedProjects();
|
|
100
|
-
const projects = Array.isArray(result.data)
|
|
101
|
-
? result.data
|
|
102
|
-
: result.data?.list ?? [];
|
|
101
|
+
const projects = Array.isArray(result.data) ? result.data : [];
|
|
103
102
|
return jsonResult({
|
|
104
103
|
projects: projects.map((p) => ({
|
|
105
104
|
id: p._id,
|
|
@@ -112,13 +111,11 @@ export function registerProjectCommands(registry) {
|
|
|
112
111
|
}, '列出关注的项目', { costHint: 'low', nextBestTools: ['project-list', 'interface-list'] });
|
|
113
112
|
// ==================== 搜索项目 ====================
|
|
114
113
|
registry.tool('project-search', {
|
|
115
|
-
query: z.string().describe('搜索关键词,必填。按项目名称做模糊匹配,空字符串会被 YApi 服务端忽略或视为列出所有项目。区分大小写由服务端规则决定。'),
|
|
114
|
+
query: z.string().trim().min(1).describe('搜索关键词,必填。按项目名称做模糊匹配,空字符串会被 YApi 服务端忽略或视为列出所有项目。区分大小写由服务端规则决定。'),
|
|
116
115
|
}, async (input) => {
|
|
117
116
|
const { api } = createYApiServices();
|
|
118
117
|
const result = await api.searchProjects(input.query);
|
|
119
|
-
const projects = Array.isArray(result.data)
|
|
120
|
-
? result.data
|
|
121
|
-
: result.data?.list ?? [];
|
|
118
|
+
const projects = Array.isArray(result.data) ? result.data : [];
|
|
122
119
|
return jsonResult({
|
|
123
120
|
query: input.query,
|
|
124
121
|
total: projects.length,
|
|
@@ -135,13 +132,17 @@ export function registerProjectCommands(registry) {
|
|
|
135
132
|
// ==================== 重新生成项目 Token ====================
|
|
136
133
|
registry.tool('project-token-update', {
|
|
137
134
|
...projectIdSchema,
|
|
135
|
+
...confirmSchema,
|
|
138
136
|
}, async (input) => {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
const payload = input;
|
|
138
|
+
return runWithPreview('project-token-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
139
|
+
const { api } = createYApiServices();
|
|
140
|
+
const result = await api.updateProjectToken(input.projectId);
|
|
141
|
+
return jsonResult({
|
|
142
|
+
projectId: input.projectId,
|
|
143
|
+
token: result.data.token,
|
|
144
|
+
message: '项目 Token 已重新生成',
|
|
145
|
+
});
|
|
145
146
|
});
|
|
146
147
|
}, '重新生成项目 Token(旧的将失效)', { costHint: 'medium', nextBestTools: ['project-token', 'project-get'] });
|
|
147
148
|
// ==================== 更新项目环境 ====================
|
|
@@ -150,110 +151,160 @@ export function registerProjectCommands(registry) {
|
|
|
150
151
|
envs: z
|
|
151
152
|
.string()
|
|
152
153
|
.describe('环境数组的 JSON 字符串,必填,格式如 [{"name":"prod","domain":"https://api.example.com"}]。每项必须包含字符串类型的 name 和 domain;该命令会整体覆盖现有环境,不会合并。建议先调用 env 获取当前配置,再追加修改。'),
|
|
154
|
+
...confirmSchema,
|
|
153
155
|
}, async (input) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (typeof e !== 'object' ||
|
|
162
|
-
e === null ||
|
|
163
|
-
typeof e.name !== 'string' ||
|
|
164
|
-
typeof e.domain !== 'string') {
|
|
165
|
-
throw new Error('envs 数组项必须包含 name 和 domain 字符串字段');
|
|
156
|
+
const payload = input;
|
|
157
|
+
return runWithPreview('project-env-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
158
|
+
let envs;
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(input.envs);
|
|
161
|
+
if (!Array.isArray(parsed)) {
|
|
162
|
+
throw new Error('envs 必须是数组');
|
|
166
163
|
}
|
|
167
|
-
|
|
164
|
+
envs = parsed.map((e) => {
|
|
165
|
+
if (typeof e !== 'object' ||
|
|
166
|
+
e === null ||
|
|
167
|
+
typeof e.name !== 'string' ||
|
|
168
|
+
typeof e.domain !== 'string') {
|
|
169
|
+
throw new Error('envs 数组项必须包含 name 和 domain 字符串字段');
|
|
170
|
+
}
|
|
171
|
+
const record = e;
|
|
172
|
+
return { name: record.name, domain: record.domain };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
throw new CliValidationError(`envs 解析失败: ${err instanceof Error ? err.message : String(err)}`, '请提供合法的 JSON 数组,例如: --envs \'[{"name":"prod","domain":"https://api.example.com"}]\'');
|
|
177
|
+
}
|
|
178
|
+
const { api } = createYApiServices();
|
|
179
|
+
await api.updateProjectEnv({
|
|
180
|
+
id: input.projectId,
|
|
181
|
+
env: envs,
|
|
182
|
+
});
|
|
183
|
+
return jsonResult({
|
|
184
|
+
projectId: input.projectId,
|
|
185
|
+
envs,
|
|
186
|
+
message: '项目环境已更新',
|
|
168
187
|
});
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
throw new CliValidationError(`envs 解析失败: ${err instanceof Error ? err.message : String(err)}`, '请提供合法的 JSON 数组,例如: --envs \'[{"name":"prod","domain":"https://api.example.com"}]\'');
|
|
172
|
-
}
|
|
173
|
-
const { api } = createYApiServices();
|
|
174
|
-
await api.updateProjectEnv({
|
|
175
|
-
project_id: input.projectId,
|
|
176
|
-
env: envs,
|
|
177
|
-
});
|
|
178
|
-
return jsonResult({
|
|
179
|
-
projectId: input.projectId,
|
|
180
|
-
envs,
|
|
181
|
-
message: '项目环境已更新',
|
|
182
188
|
});
|
|
183
189
|
}, '更新项目环境配置(覆盖现有环境)', { costHint: 'medium', nextBestTools: ['env', 'project-get'] });
|
|
184
190
|
// ==================== 更新项目标签 ====================
|
|
185
191
|
registry.tool('project-tag-update', {
|
|
186
192
|
...projectIdSchema,
|
|
187
|
-
tags: z.string().describe('
|
|
193
|
+
tags: z.string().trim().min(1).describe('标签列表,必填。支持两种格式:1) 逗号分隔的名称,例如 "订单,支付,用户";2) JSON 对象数组字符串,例如 \'[{"name":"订单","desc":"订单相关"}]\'。CLI 会整体覆盖原标签,不是追加。'),
|
|
194
|
+
...confirmSchema,
|
|
188
195
|
}, async (input) => {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
const payload = input;
|
|
197
|
+
return runWithPreview('project-tag-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
198
|
+
let tag;
|
|
199
|
+
const raw = input.tags.trim();
|
|
200
|
+
if (raw.startsWith('[')) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(raw);
|
|
203
|
+
if (!Array.isArray(parsed)) {
|
|
204
|
+
throw new Error('tags 必须是数组');
|
|
205
|
+
}
|
|
206
|
+
tag = parsed.map((item) => {
|
|
207
|
+
if (typeof item === 'string') {
|
|
208
|
+
return { name: item.trim() };
|
|
209
|
+
}
|
|
210
|
+
if (typeof item !== 'object' || item === null || typeof item.name !== 'string') {
|
|
211
|
+
throw new Error('标签对象必须包含 name 字符串字段');
|
|
212
|
+
}
|
|
213
|
+
const record = item;
|
|
214
|
+
return {
|
|
215
|
+
name: record.name,
|
|
216
|
+
desc: record.desc,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
throw new CliValidationError(`tags JSON 解析失败: ${err instanceof Error ? err.message : String(err)}`, '请提供逗号分隔的名称或 JSON 对象数组');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
tag = raw
|
|
226
|
+
.split(',')
|
|
227
|
+
.map((t) => t.trim())
|
|
228
|
+
.filter((t) => t.length > 0)
|
|
229
|
+
.map((name) => ({ name }));
|
|
230
|
+
}
|
|
231
|
+
if (tag.length === 0) {
|
|
232
|
+
throw new CliValidationError('tags 不能为空', '请提供至少一个标签,例如: --tags 订单,支付');
|
|
233
|
+
}
|
|
234
|
+
const { api } = createYApiServices();
|
|
235
|
+
await api.updateProjectTag({
|
|
236
|
+
id: input.projectId,
|
|
237
|
+
tag,
|
|
238
|
+
});
|
|
239
|
+
return jsonResult({
|
|
240
|
+
projectId: input.projectId,
|
|
241
|
+
tags: tag,
|
|
242
|
+
message: '项目标签已更新',
|
|
243
|
+
});
|
|
205
244
|
});
|
|
206
245
|
}, '更新项目标签(覆盖现有标签)', { costHint: 'medium', nextBestTools: ['project-get', 'project-list'] });
|
|
207
246
|
// ==================== 删除项目 ====================
|
|
208
247
|
registry.tool('project-delete', {
|
|
209
248
|
...projectIdSchema,
|
|
249
|
+
...confirmSchema,
|
|
210
250
|
}, async (input) => {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
251
|
+
const payload = input;
|
|
252
|
+
return runWithPreview('project-delete', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
253
|
+
const { api } = createYApiServices();
|
|
254
|
+
await api.deleteProject(input.projectId);
|
|
255
|
+
return jsonResult({
|
|
256
|
+
action: 'delete',
|
|
257
|
+
projectId: input.projectId,
|
|
258
|
+
deleted: true,
|
|
259
|
+
});
|
|
217
260
|
});
|
|
218
261
|
}, '删除项目(不可恢复)', { costHint: 'medium', nextBestTools: ['project-list', 'project-get'] });
|
|
219
262
|
// ==================== 移除项目成员 ====================
|
|
220
263
|
registry.tool('project-member-remove', {
|
|
221
264
|
...projectIdSchema,
|
|
222
|
-
uid: z.
|
|
265
|
+
uid: z.number().int().positive().describe('要移除的成员用户 UID,必填。注意仅解除该成员与项目的关联,不会删除用户本身。'),
|
|
266
|
+
...confirmSchema,
|
|
223
267
|
}, async (input) => {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
268
|
+
const payload = input;
|
|
269
|
+
return runWithPreview('project-member-remove', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
270
|
+
const { api } = createYApiServices();
|
|
271
|
+
await api.delProjectMember({
|
|
272
|
+
id: input.projectId,
|
|
273
|
+
member_uid: input.uid,
|
|
274
|
+
});
|
|
275
|
+
return jsonResult({
|
|
276
|
+
action: 'remove',
|
|
277
|
+
projectId: input.projectId,
|
|
278
|
+
uid: input.uid,
|
|
279
|
+
removed: true,
|
|
280
|
+
});
|
|
234
281
|
});
|
|
235
282
|
}, '从项目中移除成员', { costHint: 'medium', nextBestTools: ['member', 'project-get'] });
|
|
236
283
|
// ==================== 修改项目成员角色 ====================
|
|
237
284
|
registry.tool('project-member-role', {
|
|
238
285
|
...projectIdSchema,
|
|
239
|
-
memberUid: z.
|
|
286
|
+
memberUid: z.number().int().positive().describe('要修改角色的成员用户 UID,必填,YApi 内部数字 ID。'),
|
|
240
287
|
role: z
|
|
241
288
|
.enum(['owner', 'dev', 'guest', 'developer', 'viewer'])
|
|
242
289
|
.optional()
|
|
243
290
|
.default('developer')
|
|
244
291
|
.describe('目标角色,可选枚举。默认 developer。YApi 历史插件中 dev 与 developer 同时存在表示相同权限,部分服务端版本只识别其中一个,建议优先用 developer。'),
|
|
292
|
+
...confirmSchema,
|
|
245
293
|
}, async (input) => {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
294
|
+
const payload = input;
|
|
295
|
+
return runWithPreview('project-member-role', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
296
|
+
const { api } = createYApiServices();
|
|
297
|
+
await api.changeProjectMemberRole({
|
|
298
|
+
id: input.projectId,
|
|
299
|
+
member_uid: input.memberUid,
|
|
300
|
+
role: input.role,
|
|
301
|
+
});
|
|
302
|
+
return jsonResult({
|
|
303
|
+
action: 'change-role',
|
|
304
|
+
projectId: input.projectId,
|
|
305
|
+
uid: input.memberUid,
|
|
306
|
+
role: input.role,
|
|
307
|
+
});
|
|
257
308
|
});
|
|
258
309
|
}, '修改项目成员角色', { costHint: 'medium', nextBestTools: ['member', 'project-get'] });
|
|
259
310
|
// ==================== 项目 Mock 开关(全局) ====================
|
|
@@ -263,77 +314,112 @@ export function registerProjectCommands(registry) {
|
|
|
263
314
|
.union([z.literal('true'), z.literal('false')])
|
|
264
315
|
.default('true')
|
|
265
316
|
.describe('是否开启项目级全局 Mock,可选枚举 true/false,默认 true。影响该项目的 mock 接口路由是否对外可访问,不影响单接口的 mock_custom_script。'),
|
|
317
|
+
...confirmSchema,
|
|
266
318
|
}, async (input) => {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
319
|
+
const payload = input;
|
|
320
|
+
return runWithPreview('project-mock-toggle', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
321
|
+
const { api } = createYApiServices();
|
|
322
|
+
const result = await api.upSetProject({
|
|
323
|
+
project_id: input.projectId,
|
|
324
|
+
is_mock_open: input.open === 'true',
|
|
325
|
+
});
|
|
326
|
+
if (result.errcode !== 0) {
|
|
327
|
+
throw new CliApiError(result.errmsg ?? '切换项目 Mock 开关失败');
|
|
328
|
+
}
|
|
329
|
+
return jsonResult({
|
|
330
|
+
projectId: input.projectId,
|
|
331
|
+
is_mock_open: input.open === 'true',
|
|
332
|
+
message: `项目 ${input.projectId} 全局 Mock 已${input.open === 'true' ? '开启' : '关闭'}`,
|
|
333
|
+
});
|
|
279
334
|
});
|
|
280
335
|
}, '切换项目全局 Mock 开关', { costHint: 'medium', nextBestTools: ['project-mock-script', 'project-get'] });
|
|
281
336
|
// ==================== 项目 Mock 脚本 ====================
|
|
282
337
|
registry.tool('project-mock-script', {
|
|
283
338
|
...projectIdSchema,
|
|
284
|
-
script: z.string().describe('项目级 Mock 脚本内容,必填,为 JavaScript 字符串。由 YApi 服务端在 vm 沙箱内执行,对该项目的所有 mock
|
|
339
|
+
script: z.string().describe('项目级 Mock 脚本内容,必填,为 JavaScript 字符串。由 YApi 服务端在 vm 沙箱内执行,对该项目的所有 mock 请求生效。传空字符串可清空脚本。'),
|
|
340
|
+
...confirmSchema,
|
|
285
341
|
}, async (input) => {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
342
|
+
const payload = input;
|
|
343
|
+
return runWithPreview('project-mock-script', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
344
|
+
const { api } = createYApiServices();
|
|
345
|
+
const result = await api.upSetProject({
|
|
346
|
+
project_id: input.projectId,
|
|
347
|
+
project_mock_script: input.script,
|
|
348
|
+
});
|
|
349
|
+
if (result.errcode !== 0) {
|
|
350
|
+
throw new CliApiError(result.errmsg ?? '更新项目 Mock 脚本失败');
|
|
351
|
+
}
|
|
352
|
+
return jsonResult({
|
|
353
|
+
projectId: input.projectId,
|
|
354
|
+
message: `项目 ${input.projectId} Mock 脚本已更新`,
|
|
355
|
+
});
|
|
297
356
|
});
|
|
298
357
|
}, '设置项目级 Mock 脚本', { costHint: 'medium', nextBestTools: ['project-mock-toggle', 'project-get'] });
|
|
299
358
|
// ==================== 创建项目 ====================
|
|
300
359
|
registry.tool('project-create', {
|
|
301
|
-
name: z.string().describe('项目名称,必填。CLI 会做 trim,空字符串会被拒绝。同一分组内不可重名,建议先用 project-check-name 校验。'),
|
|
302
|
-
groupId: z.
|
|
360
|
+
name: z.string().trim().min(1).describe('项目名称,必填。CLI 会做 trim,空字符串会被拒绝。同一分组内不可重名,建议先用 project-check-name 校验。'),
|
|
361
|
+
groupId: z.number().int().positive().describe('所属分组 ID,必填。新建项目必须挂在某个已存在的分组下,可通过 group-list 获取。'),
|
|
303
362
|
basepath: z.string().optional().describe('项目基础路径,可选,建议以 / 开头,例如 /api。YApi 在拼接 Mock URL 时会把该路径作为前缀,不传时服务端通常默认为 /。'),
|
|
304
363
|
projectType: z.enum(['private', 'public']).optional().default('private').describe('项目类型,可选枚举,默认 private。public 项目会出现在开放接口相关命令中,token 也会公开,谨慎使用。'),
|
|
305
364
|
desc: z.string().optional().describe('项目描述,可选。仅作为展示文案,建议不超过 200 字。'),
|
|
306
365
|
icon: z.string().optional().describe('项目图标代码,可选。具体取值由前端图标库决定,未知值通常会被前端忽略,不会影响接口数据。'),
|
|
307
366
|
color: z.string().optional().describe('项目颜色代码,可选,例如 #RRGGBB 或主题色 key,仅用于前端展示。'),
|
|
308
367
|
tags: z.string().optional().describe('项目标签,可选,逗号分隔字符串,例如 订单,支付。CLI 会按逗号拆分并 trim 后整体写入,不传或空字符串表示不设置标签。'),
|
|
368
|
+
preScript: z.string().optional().describe('项目级前置脚本,可选。在接口请求前由 YApi 服务端执行(若服务端支持)。'),
|
|
369
|
+
afterScript: z.string().optional().describe('项目级后置脚本,可选。在接口请求后由 YApi 服务端执行(若服务端支持)。'),
|
|
370
|
+
env: z.string().optional().describe('项目环境配置 JSON 数组字符串,可选。例如 \'[{"name":"prod","domain":"https://api.example.com"}]\';若服务端支持,创建时直接写入环境列表。'),
|
|
371
|
+
...confirmSchema,
|
|
309
372
|
}, async (input) => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
373
|
+
const payload = input;
|
|
374
|
+
return runWithPreview('project-create', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
375
|
+
if (!input.name.trim()) {
|
|
376
|
+
throw new CliValidationError('项目名称不能为空');
|
|
377
|
+
}
|
|
378
|
+
const tag = input.tags
|
|
379
|
+
? input.tags
|
|
380
|
+
.split(',')
|
|
381
|
+
.map((t) => t.trim())
|
|
382
|
+
.filter((t) => t.length > 0)
|
|
383
|
+
: undefined;
|
|
384
|
+
let envs;
|
|
385
|
+
if (input.env) {
|
|
386
|
+
try {
|
|
387
|
+
const parsed = JSON.parse(input.env);
|
|
388
|
+
if (!Array.isArray(parsed))
|
|
389
|
+
throw new Error('env 必须是数组');
|
|
390
|
+
envs = parsed.map((e) => {
|
|
391
|
+
if (typeof e.name !== 'string' || typeof e.domain !== 'string') {
|
|
392
|
+
throw new Error('env 数组项必须包含 name 和 domain 字符串字段');
|
|
393
|
+
}
|
|
394
|
+
return e;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
throw new CliValidationError(`env 解析失败: ${err instanceof Error ? err.message : String(err)}`, '请提供合法的 JSON 数组,例如: --env \'[{"name":"prod","domain":"https://api.example.com"}]\'');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const { api } = createYApiServices();
|
|
402
|
+
const result = await api.createProject({
|
|
403
|
+
name: input.name.trim(),
|
|
404
|
+
group_id: input.groupId,
|
|
405
|
+
basepath: input.basepath,
|
|
406
|
+
project_type: input.projectType,
|
|
407
|
+
desc: input.desc,
|
|
408
|
+
icon: input.icon,
|
|
409
|
+
color: input.color,
|
|
410
|
+
tag,
|
|
411
|
+
pre_script: input.preScript,
|
|
412
|
+
after_script: input.afterScript,
|
|
413
|
+
env: envs,
|
|
414
|
+
});
|
|
415
|
+
if (result.errcode !== 0) {
|
|
416
|
+
throw new CliApiError(result.errmsg ?? '创建项目失败');
|
|
417
|
+
}
|
|
418
|
+
return jsonResult({
|
|
419
|
+
projectId: result.data._id,
|
|
420
|
+
name: input.name,
|
|
421
|
+
message: `项目「${input.name}」创建成功`,
|
|
422
|
+
});
|
|
337
423
|
});
|
|
338
424
|
}, '创建新项目', { costHint: 'medium', nextBestTools: ['project-get', 'interface-list', 'project-env-update'] });
|
|
339
425
|
// ==================== 更新项目 ====================
|
|
@@ -345,48 +431,67 @@ export function registerProjectCommands(registry) {
|
|
|
345
431
|
icon: z.string().optional().describe('新图标代码,可选。'),
|
|
346
432
|
color: z.string().optional().describe('新颜色代码,可选。'),
|
|
347
433
|
tags: z.string().optional().describe('新标签,可选,逗号分隔字符串。CLI 会按逗号拆分并 trim 后整体覆盖原标签。'),
|
|
434
|
+
preScript: z.string().optional().describe('项目级前置脚本,可选。传空字符串可清空脚本。'),
|
|
435
|
+
afterScript: z.string().optional().describe('项目级后置脚本,可选。传空字符串可清空脚本。'),
|
|
436
|
+
projectMockScript: z.string().optional().describe('项目级全局 Mock 脚本,可选。传空字符串可清空脚本;如需仅开关 Mock 请使用 project-mock-toggle。'),
|
|
437
|
+
...confirmSchema,
|
|
348
438
|
}, async (input) => {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
439
|
+
const payload = input;
|
|
440
|
+
return runWithPreview('project-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
441
|
+
const tag = input.tags
|
|
442
|
+
? input.tags
|
|
443
|
+
.split(',')
|
|
444
|
+
.map((t) => t.trim())
|
|
445
|
+
.filter((t) => t.length > 0)
|
|
446
|
+
: undefined;
|
|
447
|
+
const { api } = createYApiServices();
|
|
448
|
+
await api.updateProject({
|
|
449
|
+
id: input.projectId,
|
|
450
|
+
name: input.name,
|
|
451
|
+
basepath: input.basepath,
|
|
452
|
+
desc: input.desc,
|
|
453
|
+
icon: input.icon,
|
|
454
|
+
color: input.color,
|
|
455
|
+
tag,
|
|
456
|
+
pre_script: input.preScript,
|
|
457
|
+
after_script: input.afterScript,
|
|
458
|
+
project_mock_script: input.projectMockScript,
|
|
459
|
+
});
|
|
460
|
+
return jsonResult({
|
|
461
|
+
projectId: input.projectId,
|
|
462
|
+
message: `项目 ${input.projectId} 已更新`,
|
|
463
|
+
});
|
|
368
464
|
});
|
|
369
465
|
}, '更新项目基础信息', { costHint: 'medium', nextBestTools: ['project-get', 'project-list'] });
|
|
370
466
|
// ==================== 复制项目 ====================
|
|
371
467
|
registry.tool('project-copy', {
|
|
372
468
|
...projectIdSchema,
|
|
469
|
+
groupId: z.number().int().positive().describe('目标分组 ID,必填。复制后的新项目将挂在该分组下,需要对该分组有编辑权限。'),
|
|
373
470
|
name: z.string().optional().describe('新项目名称,可选。不传时由服务端使用默认命名规则(通常是原名称加后缀)。同分组内仍需保持唯一,建议显式传入。'),
|
|
471
|
+
...confirmSchema,
|
|
374
472
|
}, async (input) => {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
473
|
+
const payload = input;
|
|
474
|
+
return runWithPreview('project-copy', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
475
|
+
const { api } = createYApiServices();
|
|
476
|
+
const result = await api.copyProject({
|
|
477
|
+
_id: input.projectId,
|
|
478
|
+
group_id: input.groupId,
|
|
479
|
+
name: input.name,
|
|
480
|
+
});
|
|
481
|
+
if (result.errcode !== 0) {
|
|
482
|
+
throw new CliApiError(result.errmsg ?? '复制项目失败');
|
|
483
|
+
}
|
|
484
|
+
return jsonResult({
|
|
485
|
+
sourceProjectId: input.projectId,
|
|
486
|
+
newProjectId: result.data._id,
|
|
487
|
+
message: `项目 ${input.projectId} 已复制`,
|
|
488
|
+
});
|
|
384
489
|
});
|
|
385
490
|
}, '复制项目', { costHint: 'medium', nextBestTools: ['project-get', 'project-list'] });
|
|
386
491
|
// ==================== 检查项目名是否可用 ====================
|
|
387
492
|
registry.tool('project-check-name', {
|
|
388
|
-
name: z.string().describe('待校验的项目名称,必填。会先做 trim 再传给服务端;同分组内不可重名,建议先于 project-create 调用。'),
|
|
389
|
-
groupId: z.
|
|
493
|
+
name: z.string().trim().min(1).describe('待校验的项目名称,必填。会先做 trim 再传给服务端;同分组内不可重名,建议先于 project-create 调用。'),
|
|
494
|
+
groupId: z.number().int().positive().describe('分组 ID,必填,名称可用性是相对于指定分组的,不同分组下名称可以重复。'),
|
|
390
495
|
}, async (input) => {
|
|
391
496
|
const { api } = createYApiServices();
|
|
392
497
|
const result = await api.checkProjectName(input.name, input.groupId);
|
|
@@ -399,87 +504,103 @@ export function registerProjectCommands(registry) {
|
|
|
399
504
|
}, '检查分组下项目名称是否可用', { costHint: 'low', nextBestTools: ['project-create', 'project-list'] });
|
|
400
505
|
// ==================== 获取项目 Swagger URL ====================
|
|
401
506
|
registry.tool('project-swagger-url', {
|
|
402
|
-
|
|
507
|
+
url: z.string().url().describe('Swagger 数据源的 URL,必填。YApi 服务端会从该 URL 拉取 Swagger JSON 数据并返回,用于导入前的预览或校验。'),
|
|
403
508
|
}, async (input) => {
|
|
404
509
|
const { api } = createYApiServices();
|
|
405
|
-
const result = await api.getProjectSwaggerUrl(input.
|
|
510
|
+
const result = await api.getProjectSwaggerUrl(input.url);
|
|
406
511
|
return jsonResult({
|
|
407
|
-
|
|
512
|
+
url: input.url,
|
|
408
513
|
data: result.data,
|
|
409
514
|
});
|
|
410
|
-
}, '
|
|
515
|
+
}, '从指定 URL 拉取 Swagger 数据源', { costHint: 'low', nextBestTools: ['project-get', 'interface-list'] });
|
|
411
516
|
// ==================== 添加项目成员 ====================
|
|
412
517
|
registry.tool('project-member-add', {
|
|
413
518
|
...projectIdSchema,
|
|
414
|
-
uid: z.
|
|
519
|
+
uid: z.number().int().positive().describe('要加入的成员用户 UID,必填。需要被加入用户已在 YApi 注册,否则服务端会拒绝。'),
|
|
415
520
|
role: z.string().optional().describe('成员角色,可选。常用值 owner / dev / developer / guest / viewer。具体合法值由服务端决定,传未知值通常会被服务端忽略或拒绝。'),
|
|
521
|
+
...confirmSchema,
|
|
416
522
|
}, async (input) => {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
523
|
+
const payload = input;
|
|
524
|
+
return runWithPreview('project-member-add', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
525
|
+
const { api } = createYApiServices();
|
|
526
|
+
const result = await api.addProjectMember({
|
|
527
|
+
id: input.projectId,
|
|
528
|
+
member_uids: [input.uid],
|
|
529
|
+
role: input.role,
|
|
530
|
+
});
|
|
531
|
+
if (result.errcode !== 0) {
|
|
532
|
+
throw new CliApiError(result.errmsg ?? '添加项目成员失败');
|
|
533
|
+
}
|
|
534
|
+
return jsonResult({
|
|
535
|
+
projectId: input.projectId,
|
|
536
|
+
uid: input.uid,
|
|
537
|
+
role: input.role,
|
|
538
|
+
message: `用户 ${input.uid} 已加入项目 ${input.projectId}`,
|
|
539
|
+
});
|
|
431
540
|
});
|
|
432
541
|
}, '添加项目成员', { costHint: 'medium', nextBestTools: ['member', 'project-get'] });
|
|
433
542
|
// ==================== 修改成员邮件通知 ====================
|
|
434
543
|
registry.tool('project-member-email-notice', {
|
|
435
544
|
...projectIdSchema,
|
|
436
|
-
uid: z.
|
|
545
|
+
uid: z.number().int().positive().describe('目标成员的用户 UID,必填。该成员必须是项目成员,否则服务端会返回错误。'),
|
|
437
546
|
emailNotice: z
|
|
438
547
|
.union([z.literal('true'), z.literal('false')])
|
|
439
548
|
.default('true')
|
|
440
549
|
.describe('是否开启该成员在项目内的邮件通知,可选枚举 true/false,默认 true。YApi 服务端需要正确配置 SMTP 才有效。'),
|
|
550
|
+
...confirmSchema,
|
|
441
551
|
}, async (input) => {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
552
|
+
const payload = input;
|
|
553
|
+
return runWithPreview('project-member-email-notice', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
554
|
+
const { api } = createYApiServices();
|
|
555
|
+
await api.changeProjectMemberEmailNotice({
|
|
556
|
+
project_id: input.projectId,
|
|
557
|
+
member_uid: input.uid,
|
|
558
|
+
notice: input.emailNotice === 'true',
|
|
559
|
+
});
|
|
560
|
+
return jsonResult({
|
|
561
|
+
projectId: input.projectId,
|
|
562
|
+
uid: input.uid,
|
|
563
|
+
emailNotice: input.emailNotice === 'true',
|
|
564
|
+
message: `成员 ${input.uid} 邮件通知已${input.emailNotice === 'true' ? '开启' : '关闭'}`,
|
|
565
|
+
});
|
|
453
566
|
});
|
|
454
567
|
}, '修改项目成员的邮件通知开关', { costHint: 'medium', nextBestTools: ['member', 'project-get'] });
|
|
455
568
|
// ==================== 关注项目 ====================
|
|
456
569
|
registry.tool('project-follow', {
|
|
457
570
|
...projectIdSchema,
|
|
571
|
+
...confirmSchema,
|
|
458
572
|
}, async (input) => {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
573
|
+
const payload = input;
|
|
574
|
+
return runWithPreview('project-follow', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
575
|
+
const { api } = createYApiServices();
|
|
576
|
+
const result = await api.followProject(input.projectId);
|
|
577
|
+
if (result.errcode !== 0) {
|
|
578
|
+
throw new CliApiError(result.errmsg ?? '关注项目失败');
|
|
579
|
+
}
|
|
580
|
+
return jsonResult({
|
|
581
|
+
projectId: input.projectId,
|
|
582
|
+
followed: true,
|
|
583
|
+
message: `已关注项目 ${input.projectId}`,
|
|
584
|
+
});
|
|
468
585
|
});
|
|
469
586
|
}, '关注项目', { costHint: 'medium', nextBestTools: ['project-unfollow', 'follow', 'project-get'] });
|
|
470
587
|
// ==================== 取消关注项目 ====================
|
|
471
588
|
registry.tool('project-unfollow', {
|
|
472
589
|
...projectIdSchema,
|
|
590
|
+
...confirmSchema,
|
|
473
591
|
}, async (input) => {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
592
|
+
const payload = input;
|
|
593
|
+
return runWithPreview('project-unfollow', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
|
|
594
|
+
const { api } = createYApiServices();
|
|
595
|
+
const result = await api.unfollowProject(input.projectId);
|
|
596
|
+
if (result.errcode !== 0) {
|
|
597
|
+
throw new CliApiError(result.errmsg ?? '取消关注项目失败');
|
|
598
|
+
}
|
|
599
|
+
return jsonResult({
|
|
600
|
+
projectId: input.projectId,
|
|
601
|
+
followed: false,
|
|
602
|
+
message: `已取消关注项目 ${input.projectId}`,
|
|
603
|
+
});
|
|
483
604
|
});
|
|
484
605
|
}, '取消关注项目', { costHint: 'medium', nextBestTools: ['project-follow', 'follow', 'project-get'] });
|
|
485
606
|
// ==================== 项目快照(一次调用聚合项目基础信息 + 分类 + 最近接口 + 环境) ====================
|