@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +31 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +43 -5
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/changelog.js +7 -14
  7. package/dist/core/changelog.js.map +1 -1
  8. package/dist/core/cli-output.js +7 -6
  9. package/dist/core/cli-output.js.map +1 -1
  10. package/dist/core/cli-registry.js +61 -8
  11. package/dist/core/cli-registry.js.map +1 -1
  12. package/dist/core/manifest.js +14 -4
  13. package/dist/core/manifest.js.map +1 -1
  14. package/dist/core/output.d.ts +2 -2
  15. package/dist/core/output.js +12 -45
  16. package/dist/core/output.js.map +1 -1
  17. package/dist/core/tool-registry.js +5 -0
  18. package/dist/core/tool-registry.js.map +1 -1
  19. package/dist/core/update-probe.d.ts +3 -1
  20. package/dist/core/update-probe.js +9 -4
  21. package/dist/core/update-probe.js.map +1 -1
  22. package/dist/core/url-parser.js +10 -0
  23. package/dist/core/url-parser.js.map +1 -1
  24. package/dist/install.d.ts +9 -1
  25. package/dist/install.js +74 -20
  26. package/dist/install.js.map +1 -1
  27. package/dist/manifest.json +42 -1
  28. package/dist/services/yapi/api.d.ts +255 -79
  29. package/dist/services/yapi/api.js +162 -58
  30. package/dist/services/yapi/api.js.map +1 -1
  31. package/dist/services/yapi/auth.d.ts +8 -3
  32. package/dist/services/yapi/auth.js +10 -6
  33. package/dist/services/yapi/auth.js.map +1 -1
  34. package/dist/services/yapi/authCache.js +9 -2
  35. package/dist/services/yapi/authCache.js.map +1 -1
  36. package/dist/services/yapi/config.d.ts +6 -0
  37. package/dist/services/yapi/config.js +85 -8
  38. package/dist/services/yapi/config.js.map +1 -1
  39. package/dist/services/yapi/index.d.ts +3 -3
  40. package/dist/services/yapi/index.js +2 -3
  41. package/dist/services/yapi/index.js.map +1 -1
  42. package/dist/services/yapi/types.d.ts +38 -0
  43. package/dist/tools/shared.d.ts +17 -1
  44. package/dist/tools/shared.js +25 -6
  45. package/dist/tools/shared.js.map +1 -1
  46. package/dist/tools/yapi/docs-sync.js +3 -0
  47. package/dist/tools/yapi/docs-sync.js.map +1 -1
  48. package/dist/tools/yapi/groups.d.ts +1 -1
  49. package/dist/tools/yapi/groups.js +1 -1
  50. package/dist/tools/yapi/groups.js.map +1 -1
  51. package/dist/tools/yapi/register-auth.js +116 -104
  52. package/dist/tools/yapi/register-auth.js.map +1 -1
  53. package/dist/tools/yapi/register-group.js +118 -93
  54. package/dist/tools/yapi/register-group.js.map +1 -1
  55. package/dist/tools/yapi/register-interface.js +433 -199
  56. package/dist/tools/yapi/register-interface.js.map +1 -1
  57. package/dist/tools/yapi/register-mock.d.ts +2 -0
  58. package/dist/tools/yapi/register-mock.js +240 -0
  59. package/dist/tools/yapi/register-mock.js.map +1 -0
  60. package/dist/tools/yapi/register-project.js +344 -223
  61. package/dist/tools/yapi/register-project.js.map +1 -1
  62. package/dist/tools/yapi/register-test.js +33 -25
  63. package/dist/tools/yapi/register-test.js.map +1 -1
  64. package/dist/tools/yapi/register-util.js +444 -350
  65. package/dist/tools/yapi/register-util.js.map +1 -1
  66. package/dist/tools/yapi/register.js +3 -0
  67. package/dist/tools/yapi/register.js.map +1 -1
  68. package/dist/tools/yapi/utils.d.ts +49 -0
  69. package/dist/tools/yapi/utils.js +124 -2
  70. package/dist/tools/yapi/utils.js.map +1 -1
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/package.json +18 -5
  74. package/skills/yapi-cli/SKILL.md +37 -12
  75. package/skills/yapi-cli/reference/auth.md +34 -45
  76. package/skills/yapi-cli/reference/cheatsheet.md +156 -0
  77. package/skills/yapi-cli/reference/cli.md +43 -39
  78. package/skills/yapi-cli/reference/commands.md +35 -124
  79. package/skills/yapi-cli/reference/group.md +46 -19
  80. package/skills/yapi-cli/reference/index.md +32 -0
  81. package/skills/yapi-cli/reference/install.md +30 -6
  82. package/skills/yapi-cli/reference/interface.md +71 -145
  83. package/skills/yapi-cli/reference/mock.md +93 -0
  84. package/skills/yapi-cli/reference/overview.md +7 -5
  85. package/skills/yapi-cli/reference/project.md +67 -87
  86. package/skills/yapi-cli/reference/scenarios.md +184 -0
  87. package/skills/yapi-cli/reference/test.md +20 -17
  88. package/skills/yapi-cli/reference/tooling.md +89 -0
  89. package/dist/services/yapi/cache.d.ts +0 -27
  90. package/dist/services/yapi/cache.js +0 -88
  91. 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.coerce.number().describe('项目 ID,必填。YApi 服务端分配的内部数字 ID,用于查询该项目下的环境列表。'),
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.coerce.number().describe('项目 ID,必填。返回该项目的成员 uid、username、role、email 等信息。'),
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 { 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 已重新生成',
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
- let envs;
155
- try {
156
- const parsed = JSON.parse(input.envs);
157
- if (!Array.isArray(parsed)) {
158
- throw new Error('envs 必须是数组');
159
- }
160
- envs = parsed.map((e) => {
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
- return { name: e.name, domain: e.domain };
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('标签列表,逗号分隔的字符串,必填且至少 1 个有效标签,例如: 订单,支付,用户。CLI 会按逗号拆分并 trim 后整体覆盖原标签,不是追加。空字符串会被 CLI 拒绝。'),
193
+ tags: z.string().trim().min(1).describe('标签列表,必填。支持两种格式:1) 逗号分隔的名称,例如 "订单,支付,用户";2) JSON 对象数组字符串,例如 \'[{"name":"订单","desc":"订单相关"}]\'。CLI 会整体覆盖原标签,不是追加。'),
194
+ ...confirmSchema,
188
195
  }, async (input) => {
189
- const tag = input.tags
190
- .split(',')
191
- .map((t) => t.trim())
192
- .filter((t) => t.length > 0);
193
- if (tag.length === 0) {
194
- throw new CliValidationError('tags 不能为空', '请提供至少一个标签,例如: --tags 订单,支付');
195
- }
196
- const { api } = createYApiServices();
197
- await api.updateProjectTag({
198
- project_id: input.projectId,
199
- tag,
200
- });
201
- return jsonResult({
202
- projectId: input.projectId,
203
- tags: tag,
204
- message: '项目标签已更新',
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 { api } = createYApiServices();
212
- await api.deleteProject(input.projectId);
213
- return jsonResult({
214
- action: 'delete',
215
- projectId: input.projectId,
216
- deleted: true,
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.coerce.number().describe('要移除的成员用户 UID,必填。注意仅解除该成员与项目的关联,不会删除用户本身。'),
265
+ uid: z.number().int().positive().describe('要移除的成员用户 UID,必填。注意仅解除该成员与项目的关联,不会删除用户本身。'),
266
+ ...confirmSchema,
223
267
  }, async (input) => {
224
- const { api } = createYApiServices();
225
- await api.delProjectMember({
226
- project_id: input.projectId,
227
- uid: input.uid,
228
- });
229
- return jsonResult({
230
- action: 'remove',
231
- projectId: input.projectId,
232
- uid: input.uid,
233
- removed: true,
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.coerce.number().describe('要修改角色的成员用户 UID,必填,YApi 内部数字 ID。'),
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 { api } = createYApiServices();
247
- await api.changeProjectMemberRole({
248
- project_id: input.projectId,
249
- member_uid: input.memberUid,
250
- role: input.role,
251
- });
252
- return jsonResult({
253
- action: 'change-role',
254
- projectId: input.projectId,
255
- uid: input.memberUid,
256
- role: input.role,
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 { api } = createYApiServices();
268
- const result = await api.upSetProject({
269
- project_id: input.projectId,
270
- is_mock_open: input.open === 'true',
271
- });
272
- if (result.errcode !== 0) {
273
- throw new CliApiError(result.errmsg ?? '切换项目 Mock 开关失败');
274
- }
275
- return jsonResult({
276
- projectId: input.projectId,
277
- is_mock_open: input.open === 'true',
278
- message: `项目 ${input.projectId} 全局 Mock 已${input.open === 'true' ? '开启' : '关闭'}`,
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 { api } = createYApiServices();
287
- const result = await api.upSetProject({
288
- project_id: input.projectId,
289
- project_mock_script: input.script,
290
- });
291
- if (result.errcode !== 0) {
292
- throw new CliApiError(result.errmsg ?? '更新项目 Mock 脚本失败');
293
- }
294
- return jsonResult({
295
- projectId: input.projectId,
296
- message: `项目 ${input.projectId} Mock 脚本已更新`,
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.coerce.number().describe('所属分组 ID,必填。新建项目必须挂在某个已存在的分组下,可通过 group-list 获取。'),
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
- if (!input.name.trim()) {
311
- throw new CliValidationError('项目名称不能为空');
312
- }
313
- const tag = input.tags
314
- ? input.tags
315
- .split(',')
316
- .map((t) => t.trim())
317
- .filter((t) => t.length > 0)
318
- : undefined;
319
- const { api } = createYApiServices();
320
- const result = await api.createProject({
321
- name: input.name.trim(),
322
- group_id: input.groupId,
323
- basepath: input.basepath,
324
- project_type: input.projectType,
325
- desc: input.desc,
326
- icon: input.icon,
327
- color: input.color,
328
- tag,
329
- });
330
- if (result.errcode !== 0) {
331
- throw new CliApiError(result.errmsg ?? '创建项目失败');
332
- }
333
- return jsonResult({
334
- projectId: result.data._id,
335
- name: input.name,
336
- message: `项目「${input.name}」创建成功`,
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 tag = input.tags
350
- ? input.tags
351
- .split(',')
352
- .map((t) => t.trim())
353
- .filter((t) => t.length > 0)
354
- : undefined;
355
- const { api } = createYApiServices();
356
- await api.updateProject({
357
- id: input.projectId,
358
- name: input.name,
359
- basepath: input.basepath,
360
- desc: input.desc,
361
- icon: input.icon,
362
- color: input.color,
363
- tag,
364
- });
365
- return jsonResult({
366
- projectId: input.projectId,
367
- message: `项目 ${input.projectId} 已更新`,
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 { api } = createYApiServices();
376
- const result = await api.copyProject(input.projectId, input.name);
377
- if (result.errcode !== 0) {
378
- throw new CliApiError(result.errmsg ?? '复制项目失败');
379
- }
380
- return jsonResult({
381
- sourceProjectId: input.projectId,
382
- newProjectId: result.data._id,
383
- message: `项目 ${input.projectId} 已复制`,
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.coerce.number().describe('分组 ID,必填,名称可用性是相对于指定分组的,不同分组下名称可以重复。'),
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
- ...projectIdSchema,
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.projectId);
510
+ const result = await api.getProjectSwaggerUrl(input.url);
406
511
  return jsonResult({
407
- projectId: input.projectId,
512
+ url: input.url,
408
513
  data: result.data,
409
514
  });
410
- }, '获取项目的 Swagger 数据源 URL', { costHint: 'low', nextBestTools: ['project-get', 'interface-list'] });
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.coerce.number().describe('要加入的成员用户 UID,必填。需要被加入用户已在 YApi 注册,否则服务端会拒绝。'),
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 { api } = createYApiServices();
418
- const result = await api.addProjectMember({
419
- project_id: input.projectId,
420
- uid: input.uid,
421
- role: input.role,
422
- });
423
- if (result.errcode !== 0) {
424
- throw new CliApiError(result.errmsg ?? '添加项目成员失败');
425
- }
426
- return jsonResult({
427
- projectId: input.projectId,
428
- uid: input.uid,
429
- role: input.role,
430
- message: `用户 ${input.uid} 已加入项目 ${input.projectId}`,
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.coerce.number().describe('目标成员的用户 UID,必填。该成员必须是项目成员,否则服务端会返回错误。'),
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 { api } = createYApiServices();
443
- await api.changeProjectMemberEmailNotice({
444
- project_id: input.projectId,
445
- uid: input.uid,
446
- email_notice: input.emailNotice === 'true',
447
- });
448
- return jsonResult({
449
- projectId: input.projectId,
450
- uid: input.uid,
451
- emailNotice: input.emailNotice === 'true',
452
- message: `成员 ${input.uid} 邮件通知已${input.emailNotice === 'true' ? '开启' : '关闭'}`,
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 { api } = createYApiServices();
460
- const result = await api.followProject(input.projectId);
461
- if (result.errcode !== 0) {
462
- throw new CliApiError(result.errmsg ?? '关注项目失败');
463
- }
464
- return jsonResult({
465
- projectId: input.projectId,
466
- followed: true,
467
- message: `已关注项目 ${input.projectId}`,
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 { api } = createYApiServices();
475
- const result = await api.unfollowProject(input.projectId);
476
- if (result.errcode !== 0) {
477
- throw new CliApiError(result.errmsg ?? '取消关注项目失败');
478
- }
479
- return jsonResult({
480
- projectId: input.projectId,
481
- followed: false,
482
- message: `已取消关注项目 ${input.projectId}`,
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
  // ==================== 项目快照(一次调用聚合项目基础信息 + 分类 + 最近接口 + 环境) ====================