meegle-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/README.md +215 -0
- package/RELEASE.md +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2056 -0
- package/dist/core/auth-policy.d.ts +14 -0
- package/dist/core/auth-policy.js +37 -0
- package/dist/core/cli-error.d.ts +4 -0
- package/dist/core/cli-error.js +9 -0
- package/dist/core/client-factory.d.ts +3 -0
- package/dist/core/client-factory.js +22 -0
- package/dist/core/command-guard.d.ts +16 -0
- package/dist/core/command-guard.js +38 -0
- package/dist/core/config-store.d.ts +15 -0
- package/dist/core/config-store.js +69 -0
- package/dist/core/error-handler.d.ts +1 -0
- package/dist/core/error-handler.js +52 -0
- package/dist/core/json-file.d.ts +1 -0
- package/dist/core/json-file.js +12 -0
- package/dist/core/output.d.ts +1 -0
- package/dist/core/output.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/types/config.d.ts +12 -0
- package/dist/types/config.js +1 -0
- package/package.json +43 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2056 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { runGuarded } from './core/command-guard.js';
|
|
4
|
+
import { upsertProfile } from './core/config-store.js';
|
|
5
|
+
import { readJsonFile } from './core/json-file.js';
|
|
6
|
+
import { printData } from './core/output.js';
|
|
7
|
+
import { CliError } from './core/cli-error.js';
|
|
8
|
+
const DEFAULT_BASE_URL = 'https://project.feishu.cn';
|
|
9
|
+
function parseCsv(raw) {
|
|
10
|
+
if (!raw) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return raw
|
|
14
|
+
.split(',')
|
|
15
|
+
.map((item) => item.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
function parseNumber(raw, label) {
|
|
19
|
+
const value = Number(raw);
|
|
20
|
+
if (!Number.isFinite(value)) {
|
|
21
|
+
throw new CliError(`${label} 必须是数字`, 2);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function parseInteger(raw, label) {
|
|
26
|
+
const value = parseNumber(raw, label);
|
|
27
|
+
if (!Number.isInteger(value)) {
|
|
28
|
+
throw new CliError(`${label} 必须是整数`, 2);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function parseTokenType(raw) {
|
|
33
|
+
const value = Number(raw);
|
|
34
|
+
if (value !== 0 && value !== 1) {
|
|
35
|
+
throw new CliError('tokenType 仅支持 0 或 1', 2);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function parseNumberList(raw, label) {
|
|
40
|
+
const values = parseCsv(raw).map((item) => parseInteger(item, label));
|
|
41
|
+
if (values.length === 0) {
|
|
42
|
+
throw new CliError(`${label} 不能为空`, 2);
|
|
43
|
+
}
|
|
44
|
+
return values;
|
|
45
|
+
}
|
|
46
|
+
function parseOptionalInteger(raw, label) {
|
|
47
|
+
if (raw === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return parseInteger(raw, label);
|
|
51
|
+
}
|
|
52
|
+
function parseOptionalNumber(raw, label) {
|
|
53
|
+
if (raw === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return parseNumber(raw, label);
|
|
57
|
+
}
|
|
58
|
+
function parseBoolean(raw, label) {
|
|
59
|
+
const normalized = raw.trim().toLowerCase();
|
|
60
|
+
if (normalized === 'true') {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (normalized === 'false') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
throw new CliError(`${label} 仅支持 true 或 false`, 2);
|
|
67
|
+
}
|
|
68
|
+
function parseTimestamp(raw, label) {
|
|
69
|
+
const trimmed = raw.trim();
|
|
70
|
+
if (/^\d+$/.test(trimmed)) {
|
|
71
|
+
return parseInteger(trimmed, label);
|
|
72
|
+
}
|
|
73
|
+
const timestamp = Date.parse(trimmed);
|
|
74
|
+
if (Number.isNaN(timestamp)) {
|
|
75
|
+
throw new CliError(`${label} 必须是毫秒时间戳或可解析日期`, 2);
|
|
76
|
+
}
|
|
77
|
+
return timestamp;
|
|
78
|
+
}
|
|
79
|
+
function parseAction(raw, label) {
|
|
80
|
+
if (raw === 'confirm' || raw === 'rollback') {
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
throw new CliError(`${label} 仅支持 confirm 或 rollback`, 2);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* commander 默认会覆盖重复参数,这里统一改成累加,避免字段赋值场景只能保留最后一个值。
|
|
87
|
+
*/
|
|
88
|
+
function collectOptionValue(value, previous = []) {
|
|
89
|
+
return [...previous, value];
|
|
90
|
+
}
|
|
91
|
+
async function readOptionalJsonFile(filePath) {
|
|
92
|
+
if (!filePath) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return readJsonFile(filePath);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 统一处理内联 JSON 与文件输入,避免每个命令重复维护同一套解析分支。
|
|
99
|
+
*/
|
|
100
|
+
async function readJsonInput(args) {
|
|
101
|
+
if (args.inlineJson && args.filePath) {
|
|
102
|
+
throw new CliError(`--${args.label} 与 --${args.label}-file 不能同时使用`, 2);
|
|
103
|
+
}
|
|
104
|
+
if (args.inlineJson) {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(args.inlineJson);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new CliError(`--${args.label} 不是合法 JSON`, 2);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return readOptionalJsonFile(args.filePath);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 大段文本更适合放文件里,这里统一收口,避免每个命令重复处理互斥逻辑。
|
|
116
|
+
*/
|
|
117
|
+
async function readTextInput(args) {
|
|
118
|
+
if (args.inlineText !== undefined && args.filePath) {
|
|
119
|
+
throw new CliError(`--${args.label} 与 --${args.label}-file 不能同时使用`, 2);
|
|
120
|
+
}
|
|
121
|
+
if (args.inlineText !== undefined) {
|
|
122
|
+
return args.inlineText;
|
|
123
|
+
}
|
|
124
|
+
if (!args.filePath) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return readFile(args.filePath, 'utf8');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 统一约束“快捷参数”和“原始请求体”只能二选一,避免隐式合并导致结果不可预测。
|
|
131
|
+
*/
|
|
132
|
+
function assertInputMode(args) {
|
|
133
|
+
if (args.shortcutUsed && args.rawBodyUsed) {
|
|
134
|
+
throw new CliError(`命令 "${args.commandId}" 不能同时使用快捷参数和 --body/--body-file,请二选一。`, 2);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 统一处理空值兜底,避免初始化命令把环境变量、显式参数和默认值分散在多处。
|
|
139
|
+
*/
|
|
140
|
+
function pickFirstDefined(...values) {
|
|
141
|
+
for (const value of values) {
|
|
142
|
+
if (value?.trim()) {
|
|
143
|
+
return value.trim();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 用户在命令行里更容易写“看起来像 JSON 的值”,这里统一做宽松解析,减少不必要的引号负担。
|
|
150
|
+
*/
|
|
151
|
+
function parseCliValue(raw) {
|
|
152
|
+
const trimmed = raw.trim();
|
|
153
|
+
if (!trimmed) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
const looksLikeJson = trimmed.startsWith('{') ||
|
|
157
|
+
trimmed.startsWith('[') ||
|
|
158
|
+
trimmed.startsWith('"') ||
|
|
159
|
+
trimmed === 'true' ||
|
|
160
|
+
trimmed === 'false' ||
|
|
161
|
+
trimmed === 'null' ||
|
|
162
|
+
/^-?\d+(\.\d+)?$/.test(trimmed);
|
|
163
|
+
if (!looksLikeJson) {
|
|
164
|
+
return raw;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(trimmed);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return raw;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* `--field key=value` 是写操作里最通用的表达方式,统一解析后其他命令都能复用。
|
|
175
|
+
*/
|
|
176
|
+
function parseFieldAssignments(rawFields) {
|
|
177
|
+
return (rawFields ?? []).map((rawField) => {
|
|
178
|
+
const separatorIndex = rawField.indexOf('=');
|
|
179
|
+
if (separatorIndex <= 0) {
|
|
180
|
+
throw new CliError('字段赋值格式必须是 field_key=value', 2);
|
|
181
|
+
}
|
|
182
|
+
const fieldKey = rawField.slice(0, separatorIndex).trim();
|
|
183
|
+
const rawValue = rawField.slice(separatorIndex + 1);
|
|
184
|
+
if (!fieldKey) {
|
|
185
|
+
throw new CliError('字段 key 不能为空', 2);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
field_key: fieldKey,
|
|
189
|
+
field_value: parseCliValue(rawValue),
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 简单场景下让用户直接写 `P2` 或 `高:high`,避免为了单选字段再手写完整对象。
|
|
195
|
+
*/
|
|
196
|
+
function parseSelectLikeValue(raw) {
|
|
197
|
+
const trimmed = raw.trim();
|
|
198
|
+
if (!trimmed) {
|
|
199
|
+
throw new CliError('priority 不能为空', 2);
|
|
200
|
+
}
|
|
201
|
+
if (trimmed.startsWith('{')) {
|
|
202
|
+
const parsed = parseCliValue(trimmed);
|
|
203
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
204
|
+
return parsed;
|
|
205
|
+
}
|
|
206
|
+
throw new CliError('priority JSON 必须是对象', 2);
|
|
207
|
+
}
|
|
208
|
+
const pMatch = /^P(\d+)$/i.exec(trimmed);
|
|
209
|
+
if (pMatch) {
|
|
210
|
+
return { label: trimmed.toUpperCase(), value: pMatch[1] };
|
|
211
|
+
}
|
|
212
|
+
const separatorIndex = trimmed.indexOf(':');
|
|
213
|
+
if (separatorIndex > 0) {
|
|
214
|
+
const label = trimmed.slice(0, separatorIndex).trim();
|
|
215
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
216
|
+
if (!label || !value) {
|
|
217
|
+
throw new CliError('priority 格式必须是 label:value', 2);
|
|
218
|
+
}
|
|
219
|
+
return { label, value };
|
|
220
|
+
}
|
|
221
|
+
return { label: trimmed, value: trimmed };
|
|
222
|
+
}
|
|
223
|
+
function getFieldIdentifier(field) {
|
|
224
|
+
if (field.field_key?.trim()) {
|
|
225
|
+
return `field_key:${field.field_key.trim()}`;
|
|
226
|
+
}
|
|
227
|
+
if (field.field_alias?.trim()) {
|
|
228
|
+
return `field_alias:${field.field_alias.trim()}`;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 快捷参数与自定义字段如果指向同一字段,最终结果会高度不可预测,这里直接拒绝。
|
|
234
|
+
*/
|
|
235
|
+
function assertNoFieldConflicts(commandId, shortcutFields, customFields) {
|
|
236
|
+
const shortcutIdentifiers = new Set(shortcutFields.map(getFieldIdentifier).filter(Boolean));
|
|
237
|
+
for (const field of customFields) {
|
|
238
|
+
const identifier = getFieldIdentifier(field);
|
|
239
|
+
if (!identifier) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (shortcutIdentifiers.has(identifier)) {
|
|
243
|
+
throw new CliError(`命令 "${commandId}" 的快捷参数与 --field 指向了同一字段,请保留一种写法。`, 2);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function buildCommonShortcutFields(args) {
|
|
248
|
+
const shortcutFields = [];
|
|
249
|
+
if (args.desc !== undefined) {
|
|
250
|
+
shortcutFields.push({ field_key: 'description', field_value: args.desc });
|
|
251
|
+
}
|
|
252
|
+
if (args.owner?.trim()) {
|
|
253
|
+
shortcutFields.push({ field_key: 'owner', field_value: args.owner.trim() });
|
|
254
|
+
}
|
|
255
|
+
if (args.priority?.trim()) {
|
|
256
|
+
shortcutFields.push({
|
|
257
|
+
field_key: 'priority',
|
|
258
|
+
field_value: parseSelectLikeValue(args.priority),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const customFields = parseFieldAssignments(args.customFields);
|
|
262
|
+
assertNoFieldConflicts(args.commandId, shortcutFields, customFields);
|
|
263
|
+
return [...shortcutFields, ...customFields];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 排期字段在多个命令里都长得一样,集中解析后可以保证日期与估分语义一致。
|
|
267
|
+
*/
|
|
268
|
+
function buildSchedule(args) {
|
|
269
|
+
const estimateStartDate = args.start ? parseTimestamp(args.start, 'start') : undefined;
|
|
270
|
+
const estimateEndDate = args.end ? parseTimestamp(args.end, 'end') : undefined;
|
|
271
|
+
const points = parseOptionalNumber(args.points, 'points');
|
|
272
|
+
const isAuto = args.isAuto ? parseBoolean(args.isAuto, 'auto') : undefined;
|
|
273
|
+
if (estimateStartDate === undefined &&
|
|
274
|
+
estimateEndDate === undefined &&
|
|
275
|
+
points === undefined &&
|
|
276
|
+
isAuto === undefined) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
estimate_start_date: estimateStartDate,
|
|
281
|
+
estimate_end_date: estimateEndDate,
|
|
282
|
+
points,
|
|
283
|
+
is_auto: isAuto,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 把初始化所需的关键参数集中解析,避免调用方理解“哪些值能从环境变量补齐”。
|
|
288
|
+
*/
|
|
289
|
+
function resolveAuthInitOptions(options) {
|
|
290
|
+
const pluginId = pickFirstDefined(options.pluginId, process.env.MEEGLE_PLUGIN_ID);
|
|
291
|
+
const pluginSecret = pickFirstDefined(options.pluginSecret, process.env.MEEGLE_PLUGIN_SECRET);
|
|
292
|
+
const baseUrl = pickFirstDefined(options.baseUrl, process.env.MEEGLE_BASE_URL) ?? DEFAULT_BASE_URL;
|
|
293
|
+
const tokenTypeRaw = pickFirstDefined(options.tokenType, process.env.MEEGLE_TOKEN_TYPE) ?? '0';
|
|
294
|
+
const defaultUserKey = pickFirstDefined(options.defaultUserKey, process.env.MEEGLE_DEFAULT_USER_KEY, process.env.MEEGLE_USER_KEY);
|
|
295
|
+
if (!pluginId) {
|
|
296
|
+
throw new CliError('缺少 pluginId。请传 --plugin-id 或设置环境变量 MEEGLE_PLUGIN_ID。', 2);
|
|
297
|
+
}
|
|
298
|
+
if (!pluginSecret) {
|
|
299
|
+
throw new CliError('缺少 pluginSecret。请传 --plugin-secret 或设置环境变量 MEEGLE_PLUGIN_SECRET。', 2);
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
pluginId,
|
|
303
|
+
pluginSecret,
|
|
304
|
+
baseUrl,
|
|
305
|
+
tokenType: parseTokenType(tokenTypeRaw),
|
|
306
|
+
defaultUserKey,
|
|
307
|
+
targetProfile: options.targetProfile,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 为高频读操作生成默认请求体,避免普通查询也要手写 SDK 原始结构。
|
|
312
|
+
*/
|
|
313
|
+
function buildSpaceDetailRequest(options, userKey) {
|
|
314
|
+
const shortcutUsed = Boolean(options.projectKey || options.projectKeys || options.simpleName || options.simpleNames);
|
|
315
|
+
const rawBodyUsed = Boolean(options.body || options.bodyFile);
|
|
316
|
+
assertInputMode({ commandId: 'space.get', shortcutUsed, rawBodyUsed });
|
|
317
|
+
if (!shortcutUsed) {
|
|
318
|
+
throw new CliError('space get 至少需要 --project-key / --project-keys / --simple-name / --simple-names 之一,或传 --body/--body-file。', 2);
|
|
319
|
+
}
|
|
320
|
+
const projectKeys = [
|
|
321
|
+
...(options.projectKey ? [options.projectKey.trim()] : []),
|
|
322
|
+
...parseCsv(options.projectKeys),
|
|
323
|
+
].filter(Boolean);
|
|
324
|
+
const simpleNames = [
|
|
325
|
+
...(options.simpleName ? [options.simpleName.trim()] : []),
|
|
326
|
+
...parseCsv(options.simpleNames),
|
|
327
|
+
].filter(Boolean);
|
|
328
|
+
if (projectKeys.length === 0 && simpleNames.length === 0) {
|
|
329
|
+
throw new CliError('space get 的快捷参数不能为空', 2);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
project_keys: projectKeys.length > 0 ? projectKeys : undefined,
|
|
333
|
+
simple_names: simpleNames.length > 0 ? simpleNames : undefined,
|
|
334
|
+
user_key: userKey,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 为最常见的“按 ID 查工作项”场景补一层直观参数,避免用户理解底层 QueryWorkItemRequest。
|
|
339
|
+
*/
|
|
340
|
+
function buildWorkItemQueryRequest(options) {
|
|
341
|
+
const shortcutUsed = Boolean(options.id || options.ids || options.fields || options.expand);
|
|
342
|
+
const rawBodyUsed = Boolean(options.body || options.bodyFile);
|
|
343
|
+
assertInputMode({ commandId: 'workitem.get', shortcutUsed, rawBodyUsed });
|
|
344
|
+
if (!shortcutUsed) {
|
|
345
|
+
throw new CliError('workitem get 需要 --id/--ids,或传 --body/--body-file。', 2);
|
|
346
|
+
}
|
|
347
|
+
const workItemIds = [
|
|
348
|
+
...(options.id ? [parseInteger(options.id, 'id')] : []),
|
|
349
|
+
...(options.ids ? parseNumberList(options.ids, 'ids') : []),
|
|
350
|
+
];
|
|
351
|
+
if (workItemIds.length === 0) {
|
|
352
|
+
throw new CliError('workitem get 至少需要一个工作项 ID', 2);
|
|
353
|
+
}
|
|
354
|
+
let expand;
|
|
355
|
+
if (options.expand) {
|
|
356
|
+
try {
|
|
357
|
+
expand = JSON.parse(options.expand);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
throw new CliError('--expand 不是合法 JSON', 2);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const fields = parseCsv(options.fields);
|
|
364
|
+
return {
|
|
365
|
+
work_item_ids: workItemIds,
|
|
366
|
+
fields: fields.length > 0 ? fields : undefined,
|
|
367
|
+
expand,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* 创建和更新工作项最终都落到字段值对,这里抽一层避免每个命令重复拼装字段结构。
|
|
372
|
+
*/
|
|
373
|
+
async function buildWorkItemWriteFields(args) {
|
|
374
|
+
const desc = await readTextInput({
|
|
375
|
+
inlineText: args.desc,
|
|
376
|
+
filePath: args.descFile,
|
|
377
|
+
label: 'desc',
|
|
378
|
+
});
|
|
379
|
+
return buildCommonShortcutFields({
|
|
380
|
+
commandId: args.commandId,
|
|
381
|
+
desc,
|
|
382
|
+
owner: args.owner,
|
|
383
|
+
priority: args.priority,
|
|
384
|
+
customFields: args.customFields,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 常用创建场景只需要少量语义化参数,这里把它们稳定映射到 SDK 原始请求体。
|
|
389
|
+
*/
|
|
390
|
+
async function buildWorkItemCreateRequest(options) {
|
|
391
|
+
const rawBody = await readJsonInput({
|
|
392
|
+
inlineJson: options.body,
|
|
393
|
+
filePath: options.bodyFile,
|
|
394
|
+
label: 'body',
|
|
395
|
+
});
|
|
396
|
+
const shortcutFields = await buildWorkItemWriteFields({
|
|
397
|
+
commandId: 'workitem.create',
|
|
398
|
+
desc: options.desc,
|
|
399
|
+
descFile: options.descFile,
|
|
400
|
+
owner: options.owner,
|
|
401
|
+
priority: options.priority,
|
|
402
|
+
customFields: options.field,
|
|
403
|
+
});
|
|
404
|
+
const shortcutUsed = options.type !== undefined ||
|
|
405
|
+
options.name !== undefined ||
|
|
406
|
+
options.templateId !== undefined ||
|
|
407
|
+
options.requiredMode !== undefined ||
|
|
408
|
+
shortcutFields.length > 0;
|
|
409
|
+
assertInputMode({
|
|
410
|
+
commandId: 'workitem.create',
|
|
411
|
+
shortcutUsed,
|
|
412
|
+
rawBodyUsed: rawBody !== undefined,
|
|
413
|
+
});
|
|
414
|
+
if (rawBody) {
|
|
415
|
+
return rawBody;
|
|
416
|
+
}
|
|
417
|
+
if (!shortcutUsed) {
|
|
418
|
+
throw new CliError('workitem create 需要 --type,并结合 --name/--desc/--field 等快捷参数,或传 --body/--body-file。', 2);
|
|
419
|
+
}
|
|
420
|
+
const workItemTypeKey = options.type?.trim();
|
|
421
|
+
if (!workItemTypeKey) {
|
|
422
|
+
throw new CliError('workitem create 使用快捷参数时必须提供 --type', 2);
|
|
423
|
+
}
|
|
424
|
+
const name = options.name?.trim();
|
|
425
|
+
if (options.name !== undefined && !name) {
|
|
426
|
+
throw new CliError('name 不能为空', 2);
|
|
427
|
+
}
|
|
428
|
+
if (name) {
|
|
429
|
+
assertNoFieldConflicts('workitem.create', [{ field_key: 'name', field_value: name }], shortcutFields);
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
work_item_type_key: workItemTypeKey,
|
|
433
|
+
name: name || undefined,
|
|
434
|
+
template_id: parseOptionalInteger(options.templateId, 'template-id'),
|
|
435
|
+
required_mode: parseOptionalInteger(options.requiredMode, 'required-mode'),
|
|
436
|
+
field_value_pairs: shortcutFields.length > 0 ? shortcutFields : undefined,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* 更新命令需要保留“最少改哪些字段”这个语义,避免用户为了改单个标题就手写整段 update_fields。
|
|
441
|
+
*/
|
|
442
|
+
async function buildWorkItemUpdateRequest(options) {
|
|
443
|
+
const rawBody = await readJsonInput({
|
|
444
|
+
inlineJson: options.body,
|
|
445
|
+
filePath: options.bodyFile,
|
|
446
|
+
label: 'body',
|
|
447
|
+
});
|
|
448
|
+
const fields = await buildWorkItemWriteFields({
|
|
449
|
+
commandId: 'workitem.update',
|
|
450
|
+
desc: options.desc,
|
|
451
|
+
descFile: options.descFile,
|
|
452
|
+
owner: options.owner,
|
|
453
|
+
priority: options.priority,
|
|
454
|
+
customFields: options.field,
|
|
455
|
+
});
|
|
456
|
+
const name = options.name?.trim();
|
|
457
|
+
if (options.name !== undefined && !name) {
|
|
458
|
+
throw new CliError('name 不能为空', 2);
|
|
459
|
+
}
|
|
460
|
+
if (name) {
|
|
461
|
+
const nameField = { field_key: 'name', field_value: name };
|
|
462
|
+
assertNoFieldConflicts('workitem.update', [nameField], fields);
|
|
463
|
+
fields.unshift(nameField);
|
|
464
|
+
}
|
|
465
|
+
const shortcutUsed = options.name !== undefined || fields.length > 0;
|
|
466
|
+
assertInputMode({
|
|
467
|
+
commandId: 'workitem.update',
|
|
468
|
+
shortcutUsed,
|
|
469
|
+
rawBodyUsed: rawBody !== undefined,
|
|
470
|
+
});
|
|
471
|
+
if (rawBody) {
|
|
472
|
+
return rawBody;
|
|
473
|
+
}
|
|
474
|
+
if (fields.length === 0) {
|
|
475
|
+
throw new CliError('workitem update 需要至少一个更新内容,例如 --name/--desc/--field,或传 --body/--body-file。', 2);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
update_fields: fields,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* 评论输入如果允许多种来源同时生效,会出现一部分内容被静默覆盖,这里统一做强约束。
|
|
483
|
+
*/
|
|
484
|
+
async function buildCommentRequestBody(commandId, options) {
|
|
485
|
+
const rawBody = await readJsonInput({
|
|
486
|
+
inlineJson: options.body,
|
|
487
|
+
filePath: options.bodyFile,
|
|
488
|
+
label: 'body',
|
|
489
|
+
});
|
|
490
|
+
const content = await readTextInput({
|
|
491
|
+
inlineText: options.content,
|
|
492
|
+
filePath: options.contentFile,
|
|
493
|
+
label: 'content',
|
|
494
|
+
});
|
|
495
|
+
const richText = await readJsonInput({
|
|
496
|
+
inlineJson: options.richText,
|
|
497
|
+
filePath: options.richTextFile,
|
|
498
|
+
label: 'rich-text',
|
|
499
|
+
});
|
|
500
|
+
const normalizedRichText = richText;
|
|
501
|
+
const shortcutUsed = content !== undefined || normalizedRichText !== undefined;
|
|
502
|
+
assertInputMode({
|
|
503
|
+
commandId,
|
|
504
|
+
shortcutUsed,
|
|
505
|
+
rawBodyUsed: rawBody !== undefined,
|
|
506
|
+
});
|
|
507
|
+
if (rawBody) {
|
|
508
|
+
return rawBody;
|
|
509
|
+
}
|
|
510
|
+
if (!shortcutUsed) {
|
|
511
|
+
throw new CliError(`${commandId} 需要 --content/--content-file/--rich-text/--rich-text-file,或传 --body/--body-file。`, 2);
|
|
512
|
+
}
|
|
513
|
+
if (content !== undefined && normalizedRichText !== undefined) {
|
|
514
|
+
throw new CliError(`${commandId} 不能同时传纯文本和富文本,请二选一。`, 2);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
content,
|
|
518
|
+
rich_text: normalizedRichText,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
async function buildSubtaskWriteShared(args) {
|
|
522
|
+
const note = await readTextInput({
|
|
523
|
+
inlineText: args.note,
|
|
524
|
+
filePath: args.noteFile,
|
|
525
|
+
label: 'note',
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
note,
|
|
529
|
+
assignee: parseCsv(args.assignee),
|
|
530
|
+
schedule: buildSchedule({
|
|
531
|
+
start: args.start,
|
|
532
|
+
end: args.end,
|
|
533
|
+
points: args.points,
|
|
534
|
+
}),
|
|
535
|
+
fields: parseFieldAssignments(args.field),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* 子任务创建和更新共享了同一类字段,但请求结构不同,统一在这里分叉能避免命令层再次复制业务规则。
|
|
540
|
+
*/
|
|
541
|
+
async function buildSubtaskCreateRequest(options) {
|
|
542
|
+
const rawBody = await readJsonInput({
|
|
543
|
+
inlineJson: options.body,
|
|
544
|
+
filePath: options.bodyFile,
|
|
545
|
+
label: 'body',
|
|
546
|
+
});
|
|
547
|
+
const shared = await buildSubtaskWriteShared(options);
|
|
548
|
+
const shortcutUsed = options.nodeId !== undefined ||
|
|
549
|
+
options.name !== undefined ||
|
|
550
|
+
shared.note !== undefined ||
|
|
551
|
+
shared.assignee.length > 0 ||
|
|
552
|
+
shared.schedule !== undefined ||
|
|
553
|
+
shared.fields.length > 0;
|
|
554
|
+
assertInputMode({
|
|
555
|
+
commandId: 'subtask.create',
|
|
556
|
+
shortcutUsed,
|
|
557
|
+
rawBodyUsed: rawBody !== undefined,
|
|
558
|
+
});
|
|
559
|
+
if (rawBody) {
|
|
560
|
+
return rawBody;
|
|
561
|
+
}
|
|
562
|
+
if (!shortcutUsed) {
|
|
563
|
+
throw new CliError('subtask create 需要 --node-id、--name 等快捷参数,或传 --body/--body-file。', 2);
|
|
564
|
+
}
|
|
565
|
+
const nodeId = options.nodeId?.trim();
|
|
566
|
+
const name = options.name?.trim();
|
|
567
|
+
if (!nodeId) {
|
|
568
|
+
throw new CliError('subtask create 使用快捷参数时必须提供 --node-id', 2);
|
|
569
|
+
}
|
|
570
|
+
if (!name) {
|
|
571
|
+
throw new CliError('subtask create 使用快捷参数时必须提供 --name', 2);
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
node_id: nodeId,
|
|
575
|
+
name,
|
|
576
|
+
assignee: shared.assignee.length > 0 ? shared.assignee : undefined,
|
|
577
|
+
schedule: shared.schedule,
|
|
578
|
+
note: shared.note,
|
|
579
|
+
field_value_pairs: shared.fields.length > 0 ? shared.fields : undefined,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
async function buildSubtaskUpdateRequest(options) {
|
|
583
|
+
const rawBody = await readJsonInput({
|
|
584
|
+
inlineJson: options.body,
|
|
585
|
+
filePath: options.bodyFile,
|
|
586
|
+
label: 'body',
|
|
587
|
+
});
|
|
588
|
+
const shared = await buildSubtaskWriteShared(options);
|
|
589
|
+
const name = options.name?.trim();
|
|
590
|
+
if (options.name !== undefined && !name) {
|
|
591
|
+
throw new CliError('name 不能为空', 2);
|
|
592
|
+
}
|
|
593
|
+
const shortcutUsed = options.name !== undefined ||
|
|
594
|
+
shared.note !== undefined ||
|
|
595
|
+
shared.assignee.length > 0 ||
|
|
596
|
+
shared.schedule !== undefined ||
|
|
597
|
+
shared.fields.length > 0;
|
|
598
|
+
assertInputMode({
|
|
599
|
+
commandId: 'subtask.update',
|
|
600
|
+
shortcutUsed,
|
|
601
|
+
rawBodyUsed: rawBody !== undefined,
|
|
602
|
+
});
|
|
603
|
+
if (rawBody) {
|
|
604
|
+
return rawBody;
|
|
605
|
+
}
|
|
606
|
+
if (!shortcutUsed) {
|
|
607
|
+
throw new CliError('subtask update 需要至少一个更新内容,例如 --name/--note/--assignee/--field,或传 --body/--body-file。', 2);
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
name: name || undefined,
|
|
611
|
+
assignee: shared.assignee.length > 0 ? shared.assignee : undefined,
|
|
612
|
+
schedule: shared.schedule,
|
|
613
|
+
note: shared.note,
|
|
614
|
+
update_fields: shared.fields.length > 0 ? shared.fields : undefined,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
async function buildSubtaskOperateRequest(options) {
|
|
618
|
+
const rawBody = await readJsonInput({
|
|
619
|
+
inlineJson: options.body,
|
|
620
|
+
filePath: options.bodyFile,
|
|
621
|
+
label: 'body',
|
|
622
|
+
});
|
|
623
|
+
const shortcutUsed = options.nodeId !== undefined || options.taskId !== undefined || options.action !== undefined;
|
|
624
|
+
assertInputMode({
|
|
625
|
+
commandId: 'subtask.operate',
|
|
626
|
+
shortcutUsed,
|
|
627
|
+
rawBodyUsed: rawBody !== undefined,
|
|
628
|
+
});
|
|
629
|
+
if (rawBody) {
|
|
630
|
+
return rawBody;
|
|
631
|
+
}
|
|
632
|
+
if (!shortcutUsed) {
|
|
633
|
+
throw new CliError('subtask operate 需要 --node-id/--task-id/--action,或传 --body/--body-file。', 2);
|
|
634
|
+
}
|
|
635
|
+
const nodeId = options.nodeId?.trim();
|
|
636
|
+
if (!nodeId) {
|
|
637
|
+
throw new CliError('subtask operate 使用快捷参数时必须提供 --node-id', 2);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
node_id: nodeId,
|
|
641
|
+
task_id: parseInteger(options.taskId ?? '', 'task-id'),
|
|
642
|
+
action: parseAction(options.action ?? '', 'action'),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
async function buildWorkflowStateChangeRequest(options) {
|
|
646
|
+
const rawBody = await readJsonInput({
|
|
647
|
+
inlineJson: options.body,
|
|
648
|
+
filePath: options.bodyFile,
|
|
649
|
+
label: 'body',
|
|
650
|
+
});
|
|
651
|
+
const fields = parseFieldAssignments(options.field);
|
|
652
|
+
const shortcutUsed = options.transitionId !== undefined || fields.length > 0;
|
|
653
|
+
assertInputMode({
|
|
654
|
+
commandId: 'workflow.state-change',
|
|
655
|
+
shortcutUsed,
|
|
656
|
+
rawBodyUsed: rawBody !== undefined,
|
|
657
|
+
});
|
|
658
|
+
if (rawBody) {
|
|
659
|
+
return rawBody;
|
|
660
|
+
}
|
|
661
|
+
const transitionId = parseOptionalInteger(options.transitionId, 'transition-id');
|
|
662
|
+
if (transitionId === undefined) {
|
|
663
|
+
throw new CliError('workflow state-change 使用快捷参数时必须提供 --transition-id', 2);
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
transition_id: transitionId,
|
|
667
|
+
fields: fields.length > 0 ? fields : undefined,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
async function buildWorkflowNodeOperateRequest(options) {
|
|
671
|
+
const rawBody = await readJsonInput({
|
|
672
|
+
inlineJson: options.body,
|
|
673
|
+
filePath: options.bodyFile,
|
|
674
|
+
label: 'body',
|
|
675
|
+
});
|
|
676
|
+
const fields = parseFieldAssignments(options.field);
|
|
677
|
+
const schedule = buildSchedule({
|
|
678
|
+
start: options.start,
|
|
679
|
+
end: options.end,
|
|
680
|
+
points: options.points,
|
|
681
|
+
});
|
|
682
|
+
const nodeOwners = parseCsv(options.nodeOwners);
|
|
683
|
+
const shortcutUsed = options.action !== undefined ||
|
|
684
|
+
options.rollbackReason !== undefined ||
|
|
685
|
+
nodeOwners.length > 0 ||
|
|
686
|
+
schedule !== undefined ||
|
|
687
|
+
fields.length > 0;
|
|
688
|
+
assertInputMode({
|
|
689
|
+
commandId: 'workflow.node-operate',
|
|
690
|
+
shortcutUsed,
|
|
691
|
+
rawBodyUsed: rawBody !== undefined,
|
|
692
|
+
});
|
|
693
|
+
if (rawBody) {
|
|
694
|
+
return rawBody;
|
|
695
|
+
}
|
|
696
|
+
const action = options.action ? parseAction(options.action, 'action') : undefined;
|
|
697
|
+
if (!action) {
|
|
698
|
+
throw new CliError('workflow node-operate 使用快捷参数时必须提供 --action', 2);
|
|
699
|
+
}
|
|
700
|
+
if (action === 'rollback' && !options.rollbackReason?.trim()) {
|
|
701
|
+
throw new CliError('workflow node-operate 在 rollback 时必须提供 --rollback-reason', 2);
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
action,
|
|
705
|
+
rollback_reason: options.rollbackReason?.trim() || undefined,
|
|
706
|
+
node_owners: nodeOwners.length > 0 ? nodeOwners : undefined,
|
|
707
|
+
node_schedule: schedule,
|
|
708
|
+
fields: fields.length > 0 ? fields : undefined,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function resolveGlobal(program) {
|
|
712
|
+
return program.opts();
|
|
713
|
+
}
|
|
714
|
+
function printOk(asJson) {
|
|
715
|
+
printData({ ok: true }, asJson);
|
|
716
|
+
}
|
|
717
|
+
function registerAuth(program) {
|
|
718
|
+
const auth = program.command('auth').description('认证与 profile 管理');
|
|
719
|
+
auth
|
|
720
|
+
.command('init')
|
|
721
|
+
.description('初始化或更新 plugin 凭证 (plugin-only)')
|
|
722
|
+
.option('--plugin-id <pluginId>', 'Plugin ID,未传时读取 MEEGLE_PLUGIN_ID')
|
|
723
|
+
.option('--plugin-secret <pluginSecret>', 'Plugin Secret,未传时读取 MEEGLE_PLUGIN_SECRET')
|
|
724
|
+
.option('--base-url <baseURL>', `API 域名,默认 ${DEFAULT_BASE_URL}`)
|
|
725
|
+
.option('--token-type <tokenType>', '0=plugin_access_token, 1=virtual_plugin_token,默认 0')
|
|
726
|
+
.option('--default-user-key <userKey>', '默认 userKey,未传时读取 MEEGLE_DEFAULT_USER_KEY / MEEGLE_USER_KEY')
|
|
727
|
+
.option('--target-profile <name>', '写入的 profile 名称,默认 default', 'default')
|
|
728
|
+
.addHelpText('after', `
|
|
729
|
+
环境变量:
|
|
730
|
+
MEEGLE_PLUGIN_ID
|
|
731
|
+
MEEGLE_PLUGIN_SECRET
|
|
732
|
+
MEEGLE_BASE_URL
|
|
733
|
+
MEEGLE_TOKEN_TYPE
|
|
734
|
+
MEEGLE_DEFAULT_USER_KEY
|
|
735
|
+
MEEGLE_USER_KEY
|
|
736
|
+
`)
|
|
737
|
+
.action(async (options) => {
|
|
738
|
+
const resolved = resolveAuthInitOptions(options);
|
|
739
|
+
await upsertProfile(resolved.targetProfile, {
|
|
740
|
+
pluginId: resolved.pluginId,
|
|
741
|
+
pluginSecret: resolved.pluginSecret,
|
|
742
|
+
baseURL: resolved.baseUrl,
|
|
743
|
+
tokenType: resolved.tokenType,
|
|
744
|
+
defaultUserKey: resolved.defaultUserKey,
|
|
745
|
+
});
|
|
746
|
+
console.log(`已写入 profile "${resolved.targetProfile}",并设为当前 active profile。`);
|
|
747
|
+
console.log('下一步:');
|
|
748
|
+
console.log(` meegle --profile ${resolved.targetProfile} auth status`);
|
|
749
|
+
console.log(` meegle --profile ${resolved.targetProfile} space list`);
|
|
750
|
+
});
|
|
751
|
+
auth
|
|
752
|
+
.command('status')
|
|
753
|
+
.description('校验 plugin token 是否可获取')
|
|
754
|
+
.action(async () => {
|
|
755
|
+
const global = resolveGlobal(program);
|
|
756
|
+
const result = await runGuarded({
|
|
757
|
+
id: 'auth.status',
|
|
758
|
+
authPolicy: 'NONE',
|
|
759
|
+
endpoint: { method: 'POST', path: '/open_api/authen/plugin_token' },
|
|
760
|
+
}, global, async (ctx) => {
|
|
761
|
+
const token = await ctx.client.auth.getPluginToken();
|
|
762
|
+
const tokenPreview = token.length > 12 ? `${token.slice(0, 8)}...${token.slice(-4)}` : token;
|
|
763
|
+
return {
|
|
764
|
+
profile: ctx.profileName,
|
|
765
|
+
baseURL: ctx.profile.baseURL,
|
|
766
|
+
tokenType: ctx.profile.tokenType,
|
|
767
|
+
tokenPreview,
|
|
768
|
+
ok: true,
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
printData(result, Boolean(global.json));
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function registerSpace(program) {
|
|
775
|
+
const space = program.command('space').description('空间相关命令');
|
|
776
|
+
space
|
|
777
|
+
.command('list')
|
|
778
|
+
.description('列出当前用户可见空间')
|
|
779
|
+
.option('--order <fields>', '排序字段,逗号分隔', '-last_visited')
|
|
780
|
+
.action(async (options) => {
|
|
781
|
+
const global = resolveGlobal(program);
|
|
782
|
+
const result = await runGuarded({
|
|
783
|
+
id: 'space.list',
|
|
784
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
785
|
+
endpoint: { method: 'POST', path: '/open_api/projects' },
|
|
786
|
+
}, global, async (ctx) => ctx.client.space.listSpace({
|
|
787
|
+
user_key: ctx.userKey,
|
|
788
|
+
order: parseCsv(options.order),
|
|
789
|
+
}, { auth: ctx.auth }));
|
|
790
|
+
printData(result, Boolean(global.json));
|
|
791
|
+
});
|
|
792
|
+
space
|
|
793
|
+
.command('get')
|
|
794
|
+
.description('获取空间详情')
|
|
795
|
+
.option('--project-key <projectKey>', '单个空间 key')
|
|
796
|
+
.option('--project-keys <projectKeys>', '多个空间 key,逗号分隔')
|
|
797
|
+
.option('--simple-name <simpleName>', '单个空间简称')
|
|
798
|
+
.option('--simple-names <simpleNames>', '多个空间简称,逗号分隔')
|
|
799
|
+
.option('--body <json>', '内联 JSON,请求体对应 GetProjectDetailRequest')
|
|
800
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 GetProjectDetailRequest')
|
|
801
|
+
.action(async (options) => {
|
|
802
|
+
const global = resolveGlobal(program);
|
|
803
|
+
const result = await runGuarded({
|
|
804
|
+
id: 'space.get',
|
|
805
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
806
|
+
endpoint: { method: 'POST', path: '/open_api/projects/detail' },
|
|
807
|
+
}, global, async (ctx) => {
|
|
808
|
+
const rawBody = await readJsonInput({
|
|
809
|
+
inlineJson: options.body,
|
|
810
|
+
filePath: options.bodyFile,
|
|
811
|
+
label: 'body',
|
|
812
|
+
});
|
|
813
|
+
const body = rawBody ??
|
|
814
|
+
buildSpaceDetailRequest(options, ctx.userKey);
|
|
815
|
+
return ctx.client.space.getSpaceDetail({
|
|
816
|
+
...body,
|
|
817
|
+
user_key: body.user_key ?? ctx.userKey,
|
|
818
|
+
}, { auth: ctx.auth });
|
|
819
|
+
});
|
|
820
|
+
printData(result, Boolean(global.json));
|
|
821
|
+
});
|
|
822
|
+
space
|
|
823
|
+
.command('types')
|
|
824
|
+
.description('获取空间下工作项类型')
|
|
825
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
826
|
+
.action(async (options) => {
|
|
827
|
+
const global = resolveGlobal(program);
|
|
828
|
+
const result = await runGuarded({
|
|
829
|
+
id: 'space.types',
|
|
830
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
831
|
+
endpoint: { method: 'GET', path: '/open_api/:project_key/work_item/all-types' },
|
|
832
|
+
}, global, async (ctx) => ctx.client.space.listWorkItemTypes(options.projectKey, { auth: ctx.auth }));
|
|
833
|
+
printData(result, Boolean(global.json));
|
|
834
|
+
});
|
|
835
|
+
space
|
|
836
|
+
.command('team-members')
|
|
837
|
+
.description('获取空间下团队成员')
|
|
838
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
839
|
+
.action(async (options) => {
|
|
840
|
+
const global = resolveGlobal(program);
|
|
841
|
+
const result = await runGuarded({
|
|
842
|
+
id: 'space.team-members',
|
|
843
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
844
|
+
endpoint: { method: 'GET', path: '/open_api/:project_key/teams/all' },
|
|
845
|
+
}, global, async (ctx) => ctx.client.space.listTeamMembers(options.projectKey, { auth: ctx.auth }));
|
|
846
|
+
printData(result, Boolean(global.json));
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function registerWorkItem(program) {
|
|
850
|
+
const workItem = program.command('workitem').description('工作项命令');
|
|
851
|
+
workItem
|
|
852
|
+
.command('get')
|
|
853
|
+
.description('获取工作项详情')
|
|
854
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
855
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
856
|
+
.option('--id <workItemId>', '单个工作项 ID,最常用')
|
|
857
|
+
.option('--ids <workItemIds>', '多个工作项 ID,逗号分隔')
|
|
858
|
+
.option('--fields <fieldKeys>', '仅返回这些字段,逗号分隔')
|
|
859
|
+
.option('--expand <json>', '内联 JSON,对应 QueryWorkItemRequest.expand')
|
|
860
|
+
.option('--body <json>', '内联 JSON,请求体对应 QueryWorkItemRequest')
|
|
861
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 QueryWorkItemRequest')
|
|
862
|
+
.action(async (options) => {
|
|
863
|
+
const global = resolveGlobal(program);
|
|
864
|
+
const result = await runGuarded({
|
|
865
|
+
id: 'workitem.get',
|
|
866
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
867
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/work_item/:work_item_type_key/query' },
|
|
868
|
+
}, global, async (ctx) => {
|
|
869
|
+
const body = (await readJsonInput({
|
|
870
|
+
inlineJson: options.body,
|
|
871
|
+
filePath: options.bodyFile,
|
|
872
|
+
label: 'body',
|
|
873
|
+
})) ?? buildWorkItemQueryRequest(options);
|
|
874
|
+
return ctx.client.workItem.query(options.projectKey, options.type, body, { auth: ctx.auth });
|
|
875
|
+
});
|
|
876
|
+
printData(result, Boolean(global.json));
|
|
877
|
+
});
|
|
878
|
+
workItem
|
|
879
|
+
.command('meta')
|
|
880
|
+
.description('获取创建工作项元数据')
|
|
881
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
882
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
883
|
+
.action(async (options) => {
|
|
884
|
+
const global = resolveGlobal(program);
|
|
885
|
+
const result = await runGuarded({
|
|
886
|
+
id: 'workitem.meta',
|
|
887
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
888
|
+
endpoint: { method: 'GET', path: '/open_api/:project_key/work_item/:work_item_type_key/meta' },
|
|
889
|
+
}, global, async (ctx) => ctx.client.workItem.getMeta(options.projectKey, options.type, { auth: ctx.auth }));
|
|
890
|
+
printData(result, Boolean(global.json));
|
|
891
|
+
});
|
|
892
|
+
workItem
|
|
893
|
+
.command('create')
|
|
894
|
+
.description('创建工作项')
|
|
895
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
896
|
+
.option('--type <workItemTypeKey>', '工作项类型 key;快捷模式必填')
|
|
897
|
+
.option('--name <name>', '工作项标题')
|
|
898
|
+
.option('--desc <text>', '描述文本,映射到 description')
|
|
899
|
+
.option('--desc-file <path>', '从文件读取描述文本')
|
|
900
|
+
.option('--owner <userKey>', '负责人 userKey')
|
|
901
|
+
.option('--priority <priority>', '优先级,支持 P2、label:value 或 JSON 对象')
|
|
902
|
+
.option('--field <fieldSpec>', '字段赋值,格式 field_key=value;可重复', collectOptionValue, [])
|
|
903
|
+
.option('--template-id <templateId>', '流程模板 ID')
|
|
904
|
+
.option('--required-mode <mode>', '必填校验模式,0=跳过,1=校验')
|
|
905
|
+
.option('--body <json>', '内联 JSON,请求体对应 CreateWorkItemRequest')
|
|
906
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 CreateWorkItemRequest')
|
|
907
|
+
.addHelpText('after', `
|
|
908
|
+
示例:
|
|
909
|
+
meegle workitem create --project-key demo --type story --name "登录优化" --desc "补齐错误提示"
|
|
910
|
+
meegle workitem create --project-key demo --type story --field tags='[{"label":"测试","value":"test"}]'
|
|
911
|
+
`)
|
|
912
|
+
.action(async (options) => {
|
|
913
|
+
const global = resolveGlobal(program);
|
|
914
|
+
const body = await buildWorkItemCreateRequest(options);
|
|
915
|
+
const result = await runGuarded({
|
|
916
|
+
id: 'workitem.create',
|
|
917
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
918
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/work_item/create' },
|
|
919
|
+
}, global, async (ctx) => ctx.client.workItem.create(options.projectKey, body, { auth: ctx.auth }));
|
|
920
|
+
printData(result, Boolean(global.json));
|
|
921
|
+
});
|
|
922
|
+
workItem
|
|
923
|
+
.command('update')
|
|
924
|
+
.description('更新工作项')
|
|
925
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
926
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
927
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
928
|
+
.option('--name <name>', '工作项标题')
|
|
929
|
+
.option('--desc <text>', '描述文本,映射到 description')
|
|
930
|
+
.option('--desc-file <path>', '从文件读取描述文本')
|
|
931
|
+
.option('--owner <userKey>', '负责人 userKey')
|
|
932
|
+
.option('--priority <priority>', '优先级,支持 P2、label:value 或 JSON 对象')
|
|
933
|
+
.option('--field <fieldSpec>', '字段赋值,格式 field_key=value;可重复', collectOptionValue, [])
|
|
934
|
+
.option('--body <json>', '内联 JSON,请求体对应 UpdateWorkItemRequest')
|
|
935
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 UpdateWorkItemRequest')
|
|
936
|
+
.addHelpText('after', `
|
|
937
|
+
示例:
|
|
938
|
+
meegle workitem update --project-key demo --type story --id 1 --name "标题已更新"
|
|
939
|
+
meegle workitem update --project-key demo --type story --id 1 --field owner=u_demo
|
|
940
|
+
`)
|
|
941
|
+
.action(async (options) => {
|
|
942
|
+
const global = resolveGlobal(program);
|
|
943
|
+
const body = await buildWorkItemUpdateRequest(options);
|
|
944
|
+
await runGuarded({
|
|
945
|
+
id: 'workitem.update',
|
|
946
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
947
|
+
endpoint: { method: 'PUT', path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id' },
|
|
948
|
+
}, global, async (ctx) => ctx.client.workItem.update(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
949
|
+
printOk(Boolean(global.json));
|
|
950
|
+
});
|
|
951
|
+
workItem
|
|
952
|
+
.command('remove')
|
|
953
|
+
.description('删除工作项')
|
|
954
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
955
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
956
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
957
|
+
.action(async (options) => {
|
|
958
|
+
const global = resolveGlobal(program);
|
|
959
|
+
await runGuarded({
|
|
960
|
+
id: 'workitem.remove',
|
|
961
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
962
|
+
endpoint: { method: 'DELETE', path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id' },
|
|
963
|
+
}, global, async (ctx) => ctx.client.workItem.remove(options.projectKey, options.type, parseInteger(options.id, 'id'), { auth: ctx.auth }));
|
|
964
|
+
printOk(Boolean(global.json));
|
|
965
|
+
});
|
|
966
|
+
workItem
|
|
967
|
+
.command('freeze')
|
|
968
|
+
.description('冻结工作项(可批量)')
|
|
969
|
+
.requiredOption('--ids <ids>', '工作项 ID,逗号分隔')
|
|
970
|
+
.action(async (options) => {
|
|
971
|
+
const global = resolveGlobal(program);
|
|
972
|
+
await runGuarded({
|
|
973
|
+
id: 'workitem.freeze',
|
|
974
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
975
|
+
endpoint: { method: 'PUT', path: '/open_api/work_item/freeze' },
|
|
976
|
+
}, global, async (ctx) => ctx.client.workItem.freeze(parseNumberList(options.ids, 'ids'), true, { auth: ctx.auth }));
|
|
977
|
+
printOk(Boolean(global.json));
|
|
978
|
+
});
|
|
979
|
+
workItem
|
|
980
|
+
.command('unfreeze')
|
|
981
|
+
.description('解冻工作项(可批量)')
|
|
982
|
+
.requiredOption('--ids <ids>', '工作项 ID,逗号分隔')
|
|
983
|
+
.action(async (options) => {
|
|
984
|
+
const global = resolveGlobal(program);
|
|
985
|
+
await runGuarded({
|
|
986
|
+
id: 'workitem.unfreeze',
|
|
987
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
988
|
+
endpoint: { method: 'PUT', path: '/open_api/work_item/freeze' },
|
|
989
|
+
}, global, async (ctx) => ctx.client.workItem.freeze(parseNumberList(options.ids, 'ids'), false, { auth: ctx.auth }));
|
|
990
|
+
printOk(Boolean(global.json));
|
|
991
|
+
});
|
|
992
|
+
workItem
|
|
993
|
+
.command('abort')
|
|
994
|
+
.description('终止工作项')
|
|
995
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
996
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
997
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
998
|
+
.requiredOption('--reason <reason>', '终止原因')
|
|
999
|
+
.action(async (options) => {
|
|
1000
|
+
const global = resolveGlobal(program);
|
|
1001
|
+
await runGuarded({
|
|
1002
|
+
id: 'workitem.abort',
|
|
1003
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1004
|
+
endpoint: {
|
|
1005
|
+
method: 'PUT',
|
|
1006
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/abort',
|
|
1007
|
+
},
|
|
1008
|
+
}, global, async (ctx) => ctx.client.workItem.abort(options.projectKey, options.type, parseInteger(options.id, 'id'), options.reason, { auth: ctx.auth }));
|
|
1009
|
+
printOk(Boolean(global.json));
|
|
1010
|
+
});
|
|
1011
|
+
workItem
|
|
1012
|
+
.command('restore')
|
|
1013
|
+
.description('恢复工作项')
|
|
1014
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1015
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1016
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1017
|
+
.requiredOption('--reason <reason>', '恢复原因')
|
|
1018
|
+
.action(async (options) => {
|
|
1019
|
+
const global = resolveGlobal(program);
|
|
1020
|
+
await runGuarded({
|
|
1021
|
+
id: 'workitem.restore',
|
|
1022
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1023
|
+
endpoint: {
|
|
1024
|
+
method: 'PUT',
|
|
1025
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/abort',
|
|
1026
|
+
},
|
|
1027
|
+
}, global, async (ctx) => ctx.client.workItem.restore(options.projectKey, options.type, parseInteger(options.id, 'id'), options.reason, { auth: ctx.auth }));
|
|
1028
|
+
printOk(Boolean(global.json));
|
|
1029
|
+
});
|
|
1030
|
+
workItem
|
|
1031
|
+
.command('op-records')
|
|
1032
|
+
.description('获取工作项操作记录')
|
|
1033
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 GetOperationRecordsRequest')
|
|
1034
|
+
.action(async (options) => {
|
|
1035
|
+
const global = resolveGlobal(program);
|
|
1036
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1037
|
+
const result = await runGuarded({
|
|
1038
|
+
id: 'workitem.op-records',
|
|
1039
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1040
|
+
endpoint: { method: 'POST', path: '/open_api/op_record/work_item/list' },
|
|
1041
|
+
}, global, async (ctx) => ctx.client.workItem.getOperationRecords(body, { auth: ctx.auth }));
|
|
1042
|
+
printData(result, Boolean(global.json));
|
|
1043
|
+
});
|
|
1044
|
+
workItem
|
|
1045
|
+
.command('update-compound')
|
|
1046
|
+
.description('按组更新复合字段值')
|
|
1047
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 UpdateCompoundFieldRequest')
|
|
1048
|
+
.action(async (options) => {
|
|
1049
|
+
const global = resolveGlobal(program);
|
|
1050
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1051
|
+
await runGuarded({
|
|
1052
|
+
id: 'workitem.update-compound',
|
|
1053
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1054
|
+
endpoint: { method: 'POST', path: '/open_api/work_item/field_value/update_compound_field' },
|
|
1055
|
+
}, global, async (ctx) => ctx.client.workItem.updateCompoundField(body, { auth: ctx.auth }));
|
|
1056
|
+
printOk(Boolean(global.json));
|
|
1057
|
+
});
|
|
1058
|
+
const search = workItem.command('search').description('工作项搜索命令');
|
|
1059
|
+
search
|
|
1060
|
+
.command('filter')
|
|
1061
|
+
.description('单空间筛选')
|
|
1062
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1063
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 FilterWorkItemsRequest')
|
|
1064
|
+
.action(async (options) => {
|
|
1065
|
+
const global = resolveGlobal(program);
|
|
1066
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1067
|
+
const result = await runGuarded({
|
|
1068
|
+
id: 'workitem.search.filter',
|
|
1069
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1070
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/work_item/filter' },
|
|
1071
|
+
}, global, async (ctx) => ctx.client.workItem.search.filter(options.projectKey, body, { auth: ctx.auth }));
|
|
1072
|
+
printData(result, Boolean(global.json));
|
|
1073
|
+
});
|
|
1074
|
+
search
|
|
1075
|
+
.command('filter-across')
|
|
1076
|
+
.description('跨空间筛选')
|
|
1077
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 FilterWorkItemsAcrossProjectRequest')
|
|
1078
|
+
.action(async (options) => {
|
|
1079
|
+
const global = resolveGlobal(program);
|
|
1080
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1081
|
+
const result = await runGuarded({
|
|
1082
|
+
id: 'workitem.search.filter-across',
|
|
1083
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1084
|
+
endpoint: { method: 'POST', path: '/open_api/work_items/filter_across_project' },
|
|
1085
|
+
}, global, async (ctx) => ctx.client.workItem.search.filterAcrossProject(body, { auth: ctx.auth }));
|
|
1086
|
+
printData(result, Boolean(global.json));
|
|
1087
|
+
});
|
|
1088
|
+
search
|
|
1089
|
+
.command('by-params')
|
|
1090
|
+
.description('复杂条件搜索')
|
|
1091
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1092
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1093
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 SearchWorkItemsByParamsRequest')
|
|
1094
|
+
.action(async (options) => {
|
|
1095
|
+
const global = resolveGlobal(program);
|
|
1096
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1097
|
+
const result = await runGuarded({
|
|
1098
|
+
id: 'workitem.search.by-params',
|
|
1099
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1100
|
+
endpoint: {
|
|
1101
|
+
method: 'POST',
|
|
1102
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/search/params',
|
|
1103
|
+
},
|
|
1104
|
+
}, global, async (ctx) => ctx.client.workItem.search.searchByParams(options.projectKey, options.type, body, { auth: ctx.auth }));
|
|
1105
|
+
printData(result, Boolean(global.json));
|
|
1106
|
+
});
|
|
1107
|
+
search
|
|
1108
|
+
.command('by-relation')
|
|
1109
|
+
.description('关联工作项搜索')
|
|
1110
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1111
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1112
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1113
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 SearchByRelationRequest')
|
|
1114
|
+
.action(async (options) => {
|
|
1115
|
+
const global = resolveGlobal(program);
|
|
1116
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1117
|
+
const result = await runGuarded({
|
|
1118
|
+
id: 'workitem.search.by-relation',
|
|
1119
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1120
|
+
endpoint: {
|
|
1121
|
+
method: 'POST',
|
|
1122
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/search_by_relation',
|
|
1123
|
+
},
|
|
1124
|
+
}, global, async (ctx) => ctx.client.workItem.search.searchByRelation(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1125
|
+
printData(result, Boolean(global.json));
|
|
1126
|
+
});
|
|
1127
|
+
search
|
|
1128
|
+
.command('compositive')
|
|
1129
|
+
.description('全局搜索')
|
|
1130
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 CompositiveSearchRequest')
|
|
1131
|
+
.action(async (options) => {
|
|
1132
|
+
const global = resolveGlobal(program);
|
|
1133
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1134
|
+
const result = await runGuarded({
|
|
1135
|
+
id: 'workitem.search.compositive',
|
|
1136
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1137
|
+
endpoint: { method: 'POST', path: '/open_api/compositive_search' },
|
|
1138
|
+
}, global, async (ctx) => ctx.client.workItem.search.compositiveSearch(body, { auth: ctx.auth }));
|
|
1139
|
+
printData(result, Boolean(global.json));
|
|
1140
|
+
});
|
|
1141
|
+
search
|
|
1142
|
+
.command('universal')
|
|
1143
|
+
.description('通用搜索(字段按需)')
|
|
1144
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 UniversalSearchRequest')
|
|
1145
|
+
.action(async (options) => {
|
|
1146
|
+
const global = resolveGlobal(program);
|
|
1147
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1148
|
+
const result = await runGuarded({
|
|
1149
|
+
id: 'workitem.search.universal',
|
|
1150
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1151
|
+
endpoint: { method: 'POST', path: '/open_api/view_search/universal_search' },
|
|
1152
|
+
}, global, async (ctx) => ctx.client.workItem.search.universalSearch(body, { auth: ctx.auth }));
|
|
1153
|
+
printData(result, Boolean(global.json));
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function registerWorkflow(program) {
|
|
1157
|
+
const workflow = program.command('workflow').description('工作流命令');
|
|
1158
|
+
workflow
|
|
1159
|
+
.command('query')
|
|
1160
|
+
.description('获取工作流详情')
|
|
1161
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1162
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1163
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1164
|
+
.action(async (options) => {
|
|
1165
|
+
const global = resolveGlobal(program);
|
|
1166
|
+
const result = await runGuarded({
|
|
1167
|
+
id: 'workflow.query',
|
|
1168
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1169
|
+
endpoint: {
|
|
1170
|
+
method: 'POST',
|
|
1171
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/workflow/query',
|
|
1172
|
+
},
|
|
1173
|
+
}, global, async (ctx) => ctx.client.workItem.workflow.query(options.projectKey, options.type, parseInteger(options.id, 'id'), undefined, { auth: ctx.auth }));
|
|
1174
|
+
printData(result, Boolean(global.json));
|
|
1175
|
+
});
|
|
1176
|
+
workflow
|
|
1177
|
+
.command('state-change')
|
|
1178
|
+
.description('状态流转')
|
|
1179
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1180
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1181
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1182
|
+
.option('--transition-id <transitionId>', '目标流转 ID')
|
|
1183
|
+
.option('--field <fieldSpec>', '字段赋值,格式 field_key=value;可重复', collectOptionValue, [])
|
|
1184
|
+
.option('--body <json>', '内联 JSON,请求体对应 StateChangeRequest')
|
|
1185
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 StateChangeRequest')
|
|
1186
|
+
.addHelpText('after', `
|
|
1187
|
+
示例:
|
|
1188
|
+
meegle workflow state-change --project-key demo --type story --id 1 --transition-id 12345
|
|
1189
|
+
meegle workflow state-change --project-key demo --type story --id 1 --transition-id 12345 --field description="补充流转说明"
|
|
1190
|
+
`)
|
|
1191
|
+
.action(async (options) => {
|
|
1192
|
+
const global = resolveGlobal(program);
|
|
1193
|
+
const body = await buildWorkflowStateChangeRequest(options);
|
|
1194
|
+
await runGuarded({
|
|
1195
|
+
id: 'workflow.state-change',
|
|
1196
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1197
|
+
endpoint: {
|
|
1198
|
+
method: 'POST',
|
|
1199
|
+
path: '/open_api/:project_key/workflow/:work_item_type_key/:work_item_id/node/state_change',
|
|
1200
|
+
},
|
|
1201
|
+
}, global, async (ctx) => ctx.client.workItem.workflow.stateChange(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1202
|
+
printOk(Boolean(global.json));
|
|
1203
|
+
});
|
|
1204
|
+
workflow
|
|
1205
|
+
.command('node-operate')
|
|
1206
|
+
.description('节点完成/回滚')
|
|
1207
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1208
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1209
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1210
|
+
.requiredOption('--node-id <nodeId>', '节点 ID')
|
|
1211
|
+
.option('--action <action>', 'confirm=完成,rollback=回滚')
|
|
1212
|
+
.option('--rollback-reason <reason>', '回滚原因,action=rollback 时必填')
|
|
1213
|
+
.option('--node-owners <userKeys>', '节点负责人,逗号分隔')
|
|
1214
|
+
.option('--points <n>', '节点估分')
|
|
1215
|
+
.option('--start <dateOrTimestamp>', '节点排期开始时间')
|
|
1216
|
+
.option('--end <dateOrTimestamp>', '节点排期结束时间')
|
|
1217
|
+
.option('--field <fieldSpec>', '字段赋值,格式 field_key=value;可重复', collectOptionValue, [])
|
|
1218
|
+
.option('--body <json>', '内联 JSON,请求体对应 NodeOperateRequest')
|
|
1219
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 NodeOperateRequest')
|
|
1220
|
+
.addHelpText('after', `
|
|
1221
|
+
示例:
|
|
1222
|
+
meegle workflow node-operate --project-key demo --type story --id 1 --node-id dev --action confirm
|
|
1223
|
+
meegle workflow node-operate --project-key demo --type story --id 1 --node-id dev --action rollback --rollback-reason "需要返工"
|
|
1224
|
+
`)
|
|
1225
|
+
.action(async (options) => {
|
|
1226
|
+
const global = resolveGlobal(program);
|
|
1227
|
+
const body = await buildWorkflowNodeOperateRequest(options);
|
|
1228
|
+
await runGuarded({
|
|
1229
|
+
id: 'workflow.node-operate',
|
|
1230
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1231
|
+
endpoint: {
|
|
1232
|
+
method: 'POST',
|
|
1233
|
+
path: '/open_api/:project_key/workflow/:work_item_type_key/:work_item_id/node/:node_id/operate',
|
|
1234
|
+
},
|
|
1235
|
+
}, global, async (ctx) => ctx.client.workItem.workflow.operateNode(options.projectKey, options.type, parseInteger(options.id, 'id'), options.nodeId, body, { auth: ctx.auth }));
|
|
1236
|
+
printOk(Boolean(global.json));
|
|
1237
|
+
});
|
|
1238
|
+
workflow
|
|
1239
|
+
.command('node-update')
|
|
1240
|
+
.description('更新节点信息')
|
|
1241
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1242
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1243
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1244
|
+
.requiredOption('--node-id <nodeId>', '节点 ID')
|
|
1245
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 UpdateNodeRequest')
|
|
1246
|
+
.action(async (options) => {
|
|
1247
|
+
const global = resolveGlobal(program);
|
|
1248
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1249
|
+
await runGuarded({
|
|
1250
|
+
id: 'workflow.node-update',
|
|
1251
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1252
|
+
endpoint: {
|
|
1253
|
+
method: 'PUT',
|
|
1254
|
+
path: '/open_api/:project_key/workflow/:work_item_type_key/:work_item_id/node/:node_id',
|
|
1255
|
+
},
|
|
1256
|
+
}, global, async (ctx) => ctx.client.workItem.workflow.updateNode(options.projectKey, options.type, parseInteger(options.id, 'id'), options.nodeId, body, { auth: ctx.auth }));
|
|
1257
|
+
printOk(Boolean(global.json));
|
|
1258
|
+
});
|
|
1259
|
+
workflow
|
|
1260
|
+
.command('required-info')
|
|
1261
|
+
.description('获取流转所需必填信息')
|
|
1262
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 TransitionRequiredInfoRequest')
|
|
1263
|
+
.action(async (options) => {
|
|
1264
|
+
const global = resolveGlobal(program);
|
|
1265
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1266
|
+
const result = await runGuarded({
|
|
1267
|
+
id: 'workflow.required-info',
|
|
1268
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1269
|
+
endpoint: { method: 'POST', path: '/open_api/work_item/transition_required_info/get' },
|
|
1270
|
+
}, global, async (ctx) => ctx.client.workItem.workflow.getTransitionRequiredInfo(body, { auth: ctx.auth }));
|
|
1271
|
+
printData(result, Boolean(global.json));
|
|
1272
|
+
});
|
|
1273
|
+
workflow
|
|
1274
|
+
.command('wbs')
|
|
1275
|
+
.description('获取 WBS 工作流详情')
|
|
1276
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1277
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1278
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1279
|
+
.option('--query-file <path>', 'JSON 文件,内容对应 WbsViewQueryParams')
|
|
1280
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 WbsViewRequest')
|
|
1281
|
+
.action(async (options) => {
|
|
1282
|
+
const global = resolveGlobal(program);
|
|
1283
|
+
const query = await readOptionalJsonFile(options.queryFile);
|
|
1284
|
+
const body = await readOptionalJsonFile(options.bodyFile);
|
|
1285
|
+
const result = await runGuarded({
|
|
1286
|
+
id: 'workflow.wbs',
|
|
1287
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1288
|
+
endpoint: {
|
|
1289
|
+
method: 'GET',
|
|
1290
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/wbs_view',
|
|
1291
|
+
},
|
|
1292
|
+
}, global, async (ctx) => ctx.client.workItem.workflow.getWbsView(options.projectKey, options.type, parseInteger(options.id, 'id'), { query, body }, { auth: ctx.auth }));
|
|
1293
|
+
printData(result, Boolean(global.json));
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
function registerComment(program) {
|
|
1297
|
+
const comment = program.command('comment').description('评论命令');
|
|
1298
|
+
comment
|
|
1299
|
+
.command('list')
|
|
1300
|
+
.description('查询评论')
|
|
1301
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1302
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1303
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1304
|
+
.option('--query-file <path>', 'JSON 文件,内容对应 ListCommentsQuery')
|
|
1305
|
+
.option('--page-num <n>', '页码')
|
|
1306
|
+
.option('--page-size <n>', '每页条数')
|
|
1307
|
+
.action(async (options) => {
|
|
1308
|
+
const global = resolveGlobal(program);
|
|
1309
|
+
const query = (await readOptionalJsonFile(options.queryFile)) ?? {};
|
|
1310
|
+
if (options.pageNum) {
|
|
1311
|
+
query.page_num = parseInteger(options.pageNum, 'page-num');
|
|
1312
|
+
}
|
|
1313
|
+
if (options.pageSize) {
|
|
1314
|
+
query.page_size = parseInteger(options.pageSize, 'page-size');
|
|
1315
|
+
}
|
|
1316
|
+
const result = await runGuarded({
|
|
1317
|
+
id: 'comment.list',
|
|
1318
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1319
|
+
endpoint: {
|
|
1320
|
+
method: 'GET',
|
|
1321
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/comments',
|
|
1322
|
+
},
|
|
1323
|
+
}, global, async (ctx) => ctx.client.workItem.comment.list(options.projectKey, options.type, parseInteger(options.id, 'id'), query, { auth: ctx.auth }));
|
|
1324
|
+
printData(result, Boolean(global.json));
|
|
1325
|
+
});
|
|
1326
|
+
comment
|
|
1327
|
+
.command('add')
|
|
1328
|
+
.description('添加评论')
|
|
1329
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1330
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1331
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1332
|
+
.option('--content <text>', '纯文本评论')
|
|
1333
|
+
.option('--content-file <path>', '从文件读取纯文本评论')
|
|
1334
|
+
.option('--rich-text <json>', '富文本 JSON,内容对应 CreateCommentRequest.rich_text')
|
|
1335
|
+
.option('--rich-text-file <path>', '从文件读取富文本 JSON')
|
|
1336
|
+
.option('--body <json>', '内联 JSON,请求体对应 CreateCommentRequest')
|
|
1337
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 CreateCommentRequest')
|
|
1338
|
+
.addHelpText('after', `
|
|
1339
|
+
示例:
|
|
1340
|
+
meegle comment add --project-key demo --type story --id 1 --content "收到,开始处理"
|
|
1341
|
+
`)
|
|
1342
|
+
.action(async (options) => {
|
|
1343
|
+
const global = resolveGlobal(program);
|
|
1344
|
+
const body = await buildCommentRequestBody('comment.add', options);
|
|
1345
|
+
const result = await runGuarded({
|
|
1346
|
+
id: 'comment.add',
|
|
1347
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1348
|
+
endpoint: {
|
|
1349
|
+
method: 'POST',
|
|
1350
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/comment/create',
|
|
1351
|
+
},
|
|
1352
|
+
}, global, async (ctx) => ctx.client.workItem.comment.create(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1353
|
+
printData(result, Boolean(global.json));
|
|
1354
|
+
});
|
|
1355
|
+
comment
|
|
1356
|
+
.command('update')
|
|
1357
|
+
.description('更新评论')
|
|
1358
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1359
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1360
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1361
|
+
.requiredOption('--comment-id <commentId>', '评论 ID')
|
|
1362
|
+
.option('--content <text>', '纯文本评论')
|
|
1363
|
+
.option('--content-file <path>', '从文件读取纯文本评论')
|
|
1364
|
+
.option('--rich-text <json>', '富文本 JSON,内容对应 UpdateCommentRequest.rich_text')
|
|
1365
|
+
.option('--rich-text-file <path>', '从文件读取富文本 JSON')
|
|
1366
|
+
.option('--body <json>', '内联 JSON,请求体对应 UpdateCommentRequest')
|
|
1367
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 UpdateCommentRequest')
|
|
1368
|
+
.addHelpText('after', `
|
|
1369
|
+
示例:
|
|
1370
|
+
meegle comment update --project-key demo --type story --id 1 --comment-id 123 --content "已修正"
|
|
1371
|
+
`)
|
|
1372
|
+
.action(async (options) => {
|
|
1373
|
+
const global = resolveGlobal(program);
|
|
1374
|
+
const body = await buildCommentRequestBody('comment.update', options);
|
|
1375
|
+
await runGuarded({
|
|
1376
|
+
id: 'comment.update',
|
|
1377
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1378
|
+
endpoint: {
|
|
1379
|
+
method: 'PUT',
|
|
1380
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/comment/:comment_id',
|
|
1381
|
+
},
|
|
1382
|
+
}, global, async (ctx) => ctx.client.workItem.comment.update(options.projectKey, options.type, parseInteger(options.id, 'id'), options.commentId, body, { auth: ctx.auth }));
|
|
1383
|
+
printOk(Boolean(global.json));
|
|
1384
|
+
});
|
|
1385
|
+
comment
|
|
1386
|
+
.command('remove')
|
|
1387
|
+
.description('删除评论')
|
|
1388
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1389
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1390
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1391
|
+
.requiredOption('--comment-id <commentId>', '评论 ID')
|
|
1392
|
+
.action(async (options) => {
|
|
1393
|
+
const global = resolveGlobal(program);
|
|
1394
|
+
await runGuarded({
|
|
1395
|
+
id: 'comment.remove',
|
|
1396
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1397
|
+
endpoint: {
|
|
1398
|
+
method: 'DELETE',
|
|
1399
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/comment/:comment_id',
|
|
1400
|
+
},
|
|
1401
|
+
}, global, async (ctx) => ctx.client.workItem.comment.remove(options.projectKey, options.type, parseInteger(options.id, 'id'), options.commentId, { auth: ctx.auth }));
|
|
1402
|
+
printOk(Boolean(global.json));
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
function registerSubtask(program) {
|
|
1406
|
+
const subtask = program.command('subtask').description('子任务命令');
|
|
1407
|
+
subtask
|
|
1408
|
+
.command('list')
|
|
1409
|
+
.description('获取子任务列表')
|
|
1410
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1411
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1412
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1413
|
+
.option('--query-file <path>', 'JSON 文件,内容对应 GetSubtasksQuery')
|
|
1414
|
+
.option('--node-id <nodeId>', '节点 ID')
|
|
1415
|
+
.action(async (options) => {
|
|
1416
|
+
const global = resolveGlobal(program);
|
|
1417
|
+
const query = (await readOptionalJsonFile(options.queryFile)) ?? {};
|
|
1418
|
+
if (options.nodeId) {
|
|
1419
|
+
query.node_id = options.nodeId;
|
|
1420
|
+
}
|
|
1421
|
+
const result = await runGuarded({
|
|
1422
|
+
id: 'subtask.list',
|
|
1423
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1424
|
+
endpoint: {
|
|
1425
|
+
method: 'GET',
|
|
1426
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/workflow/task',
|
|
1427
|
+
},
|
|
1428
|
+
}, global, async (ctx) => ctx.client.workItem.subtask.list(options.projectKey, options.type, parseInteger(options.id, 'id'), query, { auth: ctx.auth }));
|
|
1429
|
+
printData(result, Boolean(global.json));
|
|
1430
|
+
});
|
|
1431
|
+
subtask
|
|
1432
|
+
.command('search')
|
|
1433
|
+
.description('跨空间搜索子任务')
|
|
1434
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 SearchSubtasksRequest')
|
|
1435
|
+
.action(async (options) => {
|
|
1436
|
+
const global = resolveGlobal(program);
|
|
1437
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1438
|
+
const result = await runGuarded({
|
|
1439
|
+
id: 'subtask.search',
|
|
1440
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1441
|
+
endpoint: { method: 'POST', path: '/open_api/work_item/subtask/search' },
|
|
1442
|
+
}, global, async (ctx) => ctx.client.workItem.subtask.search(body, { auth: ctx.auth }));
|
|
1443
|
+
printData(result, Boolean(global.json));
|
|
1444
|
+
});
|
|
1445
|
+
subtask
|
|
1446
|
+
.command('create')
|
|
1447
|
+
.description('创建子任务')
|
|
1448
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1449
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1450
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1451
|
+
.option('--node-id <nodeId>', '目标节点 ID')
|
|
1452
|
+
.option('--name <name>', '子任务名称')
|
|
1453
|
+
.option('--note <text>', '备注文本')
|
|
1454
|
+
.option('--note-file <path>', '从文件读取备注文本')
|
|
1455
|
+
.option('--assignee <userKeys>', '负责人 userKey,逗号分隔')
|
|
1456
|
+
.option('--points <n>', '估分')
|
|
1457
|
+
.option('--start <dateOrTimestamp>', '排期开始时间')
|
|
1458
|
+
.option('--end <dateOrTimestamp>', '排期结束时间')
|
|
1459
|
+
.option('--field <fieldSpec>', '字段赋值,格式 field_key=value;可重复', collectOptionValue, [])
|
|
1460
|
+
.option('--body <json>', '内联 JSON,请求体对应 CreateSubtaskRequest')
|
|
1461
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 CreateSubtaskRequest')
|
|
1462
|
+
.addHelpText('after', `
|
|
1463
|
+
示例:
|
|
1464
|
+
meegle subtask create --project-key demo --type story --id 1 --node-id doing --name "需求分析"
|
|
1465
|
+
`)
|
|
1466
|
+
.action(async (options) => {
|
|
1467
|
+
const global = resolveGlobal(program);
|
|
1468
|
+
const body = await buildSubtaskCreateRequest(options);
|
|
1469
|
+
const result = await runGuarded({
|
|
1470
|
+
id: 'subtask.create',
|
|
1471
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1472
|
+
endpoint: {
|
|
1473
|
+
method: 'POST',
|
|
1474
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/workflow/task',
|
|
1475
|
+
},
|
|
1476
|
+
}, global, async (ctx) => ctx.client.workItem.subtask.create(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1477
|
+
printData(result, Boolean(global.json));
|
|
1478
|
+
});
|
|
1479
|
+
subtask
|
|
1480
|
+
.command('update')
|
|
1481
|
+
.description('更新子任务')
|
|
1482
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1483
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1484
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1485
|
+
.requiredOption('--node-id <nodeId>', '节点 ID')
|
|
1486
|
+
.requiredOption('--task-id <taskId>', '子任务 ID')
|
|
1487
|
+
.option('--name <name>', '子任务名称')
|
|
1488
|
+
.option('--note <text>', '备注文本')
|
|
1489
|
+
.option('--note-file <path>', '从文件读取备注文本')
|
|
1490
|
+
.option('--assignee <userKeys>', '负责人 userKey,逗号分隔')
|
|
1491
|
+
.option('--points <n>', '估分')
|
|
1492
|
+
.option('--start <dateOrTimestamp>', '排期开始时间')
|
|
1493
|
+
.option('--end <dateOrTimestamp>', '排期结束时间')
|
|
1494
|
+
.option('--field <fieldSpec>', '字段赋值,格式 field_key=value;可重复', collectOptionValue, [])
|
|
1495
|
+
.option('--body <json>', '内联 JSON,请求体对应 UpdateSubtaskRequest')
|
|
1496
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 UpdateSubtaskRequest')
|
|
1497
|
+
.addHelpText('after', `
|
|
1498
|
+
示例:
|
|
1499
|
+
meegle subtask update --project-key demo --type story --id 1 --node-id doing --task-id 10 --name "需求分析-已更新"
|
|
1500
|
+
`)
|
|
1501
|
+
.action(async (options) => {
|
|
1502
|
+
const global = resolveGlobal(program);
|
|
1503
|
+
const body = await buildSubtaskUpdateRequest(options);
|
|
1504
|
+
await runGuarded({
|
|
1505
|
+
id: 'subtask.update',
|
|
1506
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1507
|
+
endpoint: {
|
|
1508
|
+
method: 'POST',
|
|
1509
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/workflow/:node_id/task/:task_id',
|
|
1510
|
+
},
|
|
1511
|
+
}, global, async (ctx) => ctx.client.workItem.subtask.update(options.projectKey, options.type, parseInteger(options.id, 'id'), options.nodeId, parseInteger(options.taskId, 'task-id'), body, { auth: ctx.auth }));
|
|
1512
|
+
printOk(Boolean(global.json));
|
|
1513
|
+
});
|
|
1514
|
+
subtask
|
|
1515
|
+
.command('remove')
|
|
1516
|
+
.description('删除子任务')
|
|
1517
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1518
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1519
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1520
|
+
.requiredOption('--task-id <taskId>', '子任务 ID')
|
|
1521
|
+
.action(async (options) => {
|
|
1522
|
+
const global = resolveGlobal(program);
|
|
1523
|
+
await runGuarded({
|
|
1524
|
+
id: 'subtask.remove',
|
|
1525
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1526
|
+
endpoint: {
|
|
1527
|
+
method: 'DELETE',
|
|
1528
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/task/:task_id',
|
|
1529
|
+
},
|
|
1530
|
+
}, global, async (ctx) => ctx.client.workItem.subtask.remove(options.projectKey, options.type, parseInteger(options.id, 'id'), parseInteger(options.taskId, 'task-id'), { auth: ctx.auth }));
|
|
1531
|
+
printOk(Boolean(global.json));
|
|
1532
|
+
});
|
|
1533
|
+
subtask
|
|
1534
|
+
.command('operate')
|
|
1535
|
+
.description('完成/回滚子任务')
|
|
1536
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1537
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1538
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1539
|
+
.option('--node-id <nodeId>', '节点 ID')
|
|
1540
|
+
.option('--task-id <taskId>', '子任务 ID')
|
|
1541
|
+
.option('--action <action>', 'confirm=完成,rollback=回滚')
|
|
1542
|
+
.option('--body <json>', '内联 JSON,请求体对应 CompleteRollbackSubtaskRequest')
|
|
1543
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 CompleteRollbackSubtaskRequest')
|
|
1544
|
+
.addHelpText('after', `
|
|
1545
|
+
示例:
|
|
1546
|
+
meegle subtask operate --project-key demo --type story --id 1 --node-id doing --task-id 10 --action confirm
|
|
1547
|
+
`)
|
|
1548
|
+
.action(async (options) => {
|
|
1549
|
+
const global = resolveGlobal(program);
|
|
1550
|
+
const body = await buildSubtaskOperateRequest(options);
|
|
1551
|
+
await runGuarded({
|
|
1552
|
+
id: 'subtask.operate',
|
|
1553
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1554
|
+
endpoint: {
|
|
1555
|
+
method: 'POST',
|
|
1556
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/subtask/modify',
|
|
1557
|
+
},
|
|
1558
|
+
}, global, async (ctx) => ctx.client.workItem.subtask.completeOrRollback(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1559
|
+
printOk(Boolean(global.json));
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
function registerAttachment(program) {
|
|
1563
|
+
const attachment = program.command('attachment').description('附件命令');
|
|
1564
|
+
attachment
|
|
1565
|
+
.command('upload-file')
|
|
1566
|
+
.description('通用文件上传(富文本图片等)')
|
|
1567
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1568
|
+
.requiredOption('--file <path>', '本地文件路径')
|
|
1569
|
+
.option('--file-name <name>', '上传文件名')
|
|
1570
|
+
.action(async (options) => {
|
|
1571
|
+
const global = resolveGlobal(program);
|
|
1572
|
+
const file = await readFile(options.file);
|
|
1573
|
+
const result = await runGuarded({
|
|
1574
|
+
id: 'attachment.upload-file',
|
|
1575
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1576
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/file/upload' },
|
|
1577
|
+
}, global, async (ctx) => ctx.client.workItem.attachment.uploadFile(options.projectKey, { file, fileName: options.fileName }, { auth: ctx.auth }));
|
|
1578
|
+
printData(result, Boolean(global.json));
|
|
1579
|
+
});
|
|
1580
|
+
attachment
|
|
1581
|
+
.command('upload')
|
|
1582
|
+
.description('添加附件到工作项')
|
|
1583
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1584
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1585
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1586
|
+
.requiredOption('--file <path>', '本地文件路径')
|
|
1587
|
+
.option('--file-name <name>', '上传文件名')
|
|
1588
|
+
.option('--field-key <fieldKey>', '附件字段 key')
|
|
1589
|
+
.option('--field-alias <fieldAlias>', '附件字段 alias')
|
|
1590
|
+
.option('--index <index>', '复合字段索引')
|
|
1591
|
+
.action(async (options) => {
|
|
1592
|
+
const global = resolveGlobal(program);
|
|
1593
|
+
const file = await readFile(options.file);
|
|
1594
|
+
await runGuarded({
|
|
1595
|
+
id: 'attachment.upload',
|
|
1596
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1597
|
+
endpoint: {
|
|
1598
|
+
method: 'POST',
|
|
1599
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/file/upload',
|
|
1600
|
+
},
|
|
1601
|
+
}, global, async (ctx) => ctx.client.workItem.attachment.uploadAttachment(options.projectKey, options.type, parseInteger(options.id, 'id'), {
|
|
1602
|
+
file,
|
|
1603
|
+
fileName: options.fileName,
|
|
1604
|
+
field_key: options.fieldKey,
|
|
1605
|
+
field_alias: options.fieldAlias,
|
|
1606
|
+
index: options.index,
|
|
1607
|
+
}, { auth: ctx.auth }));
|
|
1608
|
+
printOk(Boolean(global.json));
|
|
1609
|
+
});
|
|
1610
|
+
attachment
|
|
1611
|
+
.command('download')
|
|
1612
|
+
.description('下载附件')
|
|
1613
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1614
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1615
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1616
|
+
.requiredOption('--uuid <uuid>', '附件 UUID')
|
|
1617
|
+
.requiredOption('--out <path>', '输出文件路径')
|
|
1618
|
+
.action(async (options) => {
|
|
1619
|
+
const global = resolveGlobal(program);
|
|
1620
|
+
await runGuarded({
|
|
1621
|
+
id: 'attachment.download',
|
|
1622
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1623
|
+
endpoint: {
|
|
1624
|
+
method: 'POST',
|
|
1625
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/file/download',
|
|
1626
|
+
},
|
|
1627
|
+
}, global, async (ctx) => {
|
|
1628
|
+
const data = await ctx.client.workItem.attachment.downloadAttachment(options.projectKey, options.type, parseInteger(options.id, 'id'), { uuid: options.uuid }, { auth: ctx.auth });
|
|
1629
|
+
await writeFile(options.out, Buffer.from(data));
|
|
1630
|
+
});
|
|
1631
|
+
printData({ ok: true, output: options.out }, Boolean(global.json));
|
|
1632
|
+
});
|
|
1633
|
+
attachment
|
|
1634
|
+
.command('delete')
|
|
1635
|
+
.description('删除附件')
|
|
1636
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 DeleteAttachmentRequest')
|
|
1637
|
+
.action(async (options) => {
|
|
1638
|
+
const global = resolveGlobal(program);
|
|
1639
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1640
|
+
await runGuarded({
|
|
1641
|
+
id: 'attachment.delete',
|
|
1642
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1643
|
+
endpoint: { method: 'POST', path: '/open_api/file/delete' },
|
|
1644
|
+
}, global, async (ctx) => ctx.client.workItem.attachment.deleteAttachment(body, { auth: ctx.auth }));
|
|
1645
|
+
printOk(Boolean(global.json));
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
function registerWorkHour(program) {
|
|
1649
|
+
const workHour = program.command('workhour').description('工时命令');
|
|
1650
|
+
workHour
|
|
1651
|
+
.command('list')
|
|
1652
|
+
.description('获取工时记录列表')
|
|
1653
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 ListWorkHourRecordsRequest')
|
|
1654
|
+
.action(async (options) => {
|
|
1655
|
+
const global = resolveGlobal(program);
|
|
1656
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1657
|
+
const result = await runGuarded({
|
|
1658
|
+
id: 'workhour.list',
|
|
1659
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1660
|
+
endpoint: { method: 'POST', path: '/open_api/work_item/man_hour/records' },
|
|
1661
|
+
}, global, async (ctx) => ctx.client.workItem.workHour.list(body, { auth: ctx.auth }));
|
|
1662
|
+
printData(result, Boolean(global.json));
|
|
1663
|
+
});
|
|
1664
|
+
workHour
|
|
1665
|
+
.command('create')
|
|
1666
|
+
.description('创建工时记录')
|
|
1667
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1668
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1669
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1670
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 CreateWorkHourRecordRequest')
|
|
1671
|
+
.action(async (options) => {
|
|
1672
|
+
const global = resolveGlobal(program);
|
|
1673
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1674
|
+
const result = await runGuarded({
|
|
1675
|
+
id: 'workhour.create',
|
|
1676
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1677
|
+
endpoint: {
|
|
1678
|
+
method: 'POST',
|
|
1679
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/work_hour_record',
|
|
1680
|
+
},
|
|
1681
|
+
}, global, async (ctx) => ctx.client.workItem.workHour.create(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1682
|
+
printData(result, Boolean(global.json));
|
|
1683
|
+
});
|
|
1684
|
+
workHour
|
|
1685
|
+
.command('update')
|
|
1686
|
+
.description('更新工时记录')
|
|
1687
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1688
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1689
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1690
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 UpdateWorkHourRecordRequest')
|
|
1691
|
+
.action(async (options) => {
|
|
1692
|
+
const global = resolveGlobal(program);
|
|
1693
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1694
|
+
await runGuarded({
|
|
1695
|
+
id: 'workhour.update',
|
|
1696
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1697
|
+
endpoint: {
|
|
1698
|
+
method: 'PUT',
|
|
1699
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/work_hour_record',
|
|
1700
|
+
},
|
|
1701
|
+
}, global, async (ctx) => ctx.client.workItem.workHour.update(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1702
|
+
printOk(Boolean(global.json));
|
|
1703
|
+
});
|
|
1704
|
+
workHour
|
|
1705
|
+
.command('delete')
|
|
1706
|
+
.description('删除工时记录')
|
|
1707
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1708
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1709
|
+
.requiredOption('--id <workItemId>', '工作项 ID')
|
|
1710
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 DeleteWorkHourRecordRequest')
|
|
1711
|
+
.action(async (options) => {
|
|
1712
|
+
const global = resolveGlobal(program);
|
|
1713
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1714
|
+
await runGuarded({
|
|
1715
|
+
id: 'workhour.delete',
|
|
1716
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1717
|
+
endpoint: {
|
|
1718
|
+
method: 'DELETE',
|
|
1719
|
+
path: '/open_api/:project_key/work_item/:work_item_type_key/:work_item_id/work_hour_record',
|
|
1720
|
+
},
|
|
1721
|
+
}, global, async (ctx) => ctx.client.workItem.workHour.deleteRecords(options.projectKey, options.type, parseInteger(options.id, 'id'), body, { auth: ctx.auth }));
|
|
1722
|
+
printOk(Boolean(global.json));
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
function registerView(program) {
|
|
1726
|
+
const view = program.command('view').description('视图命令');
|
|
1727
|
+
view
|
|
1728
|
+
.command('list')
|
|
1729
|
+
.description('获取视图列表及配置')
|
|
1730
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1731
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 ViewConfListRequest')
|
|
1732
|
+
.action(async (options) => {
|
|
1733
|
+
const global = resolveGlobal(program);
|
|
1734
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1735
|
+
const result = await runGuarded({
|
|
1736
|
+
id: 'view.list',
|
|
1737
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1738
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/view_conf/list' },
|
|
1739
|
+
}, global, async (ctx) => ctx.client.view.query.listConfigs(options.projectKey, body, { auth: ctx.auth }));
|
|
1740
|
+
printData(result, Boolean(global.json));
|
|
1741
|
+
});
|
|
1742
|
+
view
|
|
1743
|
+
.command('fix-items')
|
|
1744
|
+
.description('获取固定视图下工作项')
|
|
1745
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1746
|
+
.requiredOption('--view-id <viewId>', '视图 ID')
|
|
1747
|
+
.option('--query-file <path>', 'JSON 文件,内容对应 FixViewQueryParams')
|
|
1748
|
+
.action(async (options) => {
|
|
1749
|
+
const global = resolveGlobal(program);
|
|
1750
|
+
const query = await readOptionalJsonFile(options.queryFile);
|
|
1751
|
+
const result = await runGuarded({
|
|
1752
|
+
id: 'view.fix-items',
|
|
1753
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1754
|
+
endpoint: { method: 'GET', path: '/open_api/:project_key/fix_view/:view_id' },
|
|
1755
|
+
}, global, async (ctx) => ctx.client.view.query.getFixViewItems(options.projectKey, options.viewId, query, { auth: ctx.auth }));
|
|
1756
|
+
printData(result, Boolean(global.json));
|
|
1757
|
+
});
|
|
1758
|
+
view
|
|
1759
|
+
.command('panoramic-items')
|
|
1760
|
+
.description('获取全景视图下工作项')
|
|
1761
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1762
|
+
.requiredOption('--view-id <viewId>', '视图 ID')
|
|
1763
|
+
.option('--body-file <path>', 'JSON 文件,内容对应 PanoramicViewQueryParams')
|
|
1764
|
+
.action(async (options) => {
|
|
1765
|
+
const global = resolveGlobal(program);
|
|
1766
|
+
const body = await readOptionalJsonFile(options.bodyFile);
|
|
1767
|
+
const result = await runGuarded({
|
|
1768
|
+
id: 'view.panoramic-items',
|
|
1769
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1770
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/view/:view_id' },
|
|
1771
|
+
}, global, async (ctx) => ctx.client.view.query.listPanoramicItems(options.projectKey, options.viewId, body, { auth: ctx.auth }));
|
|
1772
|
+
printData(result, Boolean(global.json));
|
|
1773
|
+
});
|
|
1774
|
+
view
|
|
1775
|
+
.command('create-fix')
|
|
1776
|
+
.description('创建固定视图')
|
|
1777
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1778
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1779
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 CreateFixViewRequest')
|
|
1780
|
+
.action(async (options) => {
|
|
1781
|
+
const global = resolveGlobal(program);
|
|
1782
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1783
|
+
const result = await runGuarded({
|
|
1784
|
+
id: 'view.create-fix',
|
|
1785
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1786
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/:work_item_type_key/fix_view' },
|
|
1787
|
+
}, global, async (ctx) => ctx.client.view.createFixView(options.projectKey, options.type, body, { auth: ctx.auth }));
|
|
1788
|
+
printData(result, Boolean(global.json));
|
|
1789
|
+
});
|
|
1790
|
+
view
|
|
1791
|
+
.command('update-fix')
|
|
1792
|
+
.description('更新固定视图')
|
|
1793
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1794
|
+
.requiredOption('--type <workItemTypeKey>', '工作项类型 key')
|
|
1795
|
+
.requiredOption('--view-id <viewId>', '视图 ID')
|
|
1796
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 UpdateFixViewRequest')
|
|
1797
|
+
.action(async (options) => {
|
|
1798
|
+
const global = resolveGlobal(program);
|
|
1799
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1800
|
+
await runGuarded({
|
|
1801
|
+
id: 'view.update-fix',
|
|
1802
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1803
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/:work_item_type_key/fix_view/:view_id' },
|
|
1804
|
+
}, global, async (ctx) => ctx.client.view.updateFixView(options.projectKey, options.type, options.viewId, body, { auth: ctx.auth }));
|
|
1805
|
+
printOk(Boolean(global.json));
|
|
1806
|
+
});
|
|
1807
|
+
view
|
|
1808
|
+
.command('delete')
|
|
1809
|
+
.description('删除视图')
|
|
1810
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1811
|
+
.requiredOption('--view-id <viewId>', '视图 ID')
|
|
1812
|
+
.action(async (options) => {
|
|
1813
|
+
const global = resolveGlobal(program);
|
|
1814
|
+
await runGuarded({
|
|
1815
|
+
id: 'view.delete',
|
|
1816
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1817
|
+
endpoint: { method: 'DELETE', path: '/open_api/:project_key/fix_view/:view_id' },
|
|
1818
|
+
}, global, async (ctx) => ctx.client.view.deleteView(options.projectKey, options.viewId, { auth: ctx.auth }));
|
|
1819
|
+
printOk(Boolean(global.json));
|
|
1820
|
+
});
|
|
1821
|
+
view
|
|
1822
|
+
.command('create-condition')
|
|
1823
|
+
.description('创建条件视图')
|
|
1824
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 CreateConditionViewRequest')
|
|
1825
|
+
.action(async (options) => {
|
|
1826
|
+
const global = resolveGlobal(program);
|
|
1827
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1828
|
+
const result = await runGuarded({
|
|
1829
|
+
id: 'view.create-condition',
|
|
1830
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1831
|
+
endpoint: { method: 'POST', path: '/open_api/view/v1/create_condition_view' },
|
|
1832
|
+
}, global, async (ctx) => ctx.client.view.createConditionView(body, { auth: ctx.auth }));
|
|
1833
|
+
printData(result, Boolean(global.json));
|
|
1834
|
+
});
|
|
1835
|
+
view
|
|
1836
|
+
.command('update-condition')
|
|
1837
|
+
.description('更新条件视图')
|
|
1838
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 UpdateConditionViewRequest')
|
|
1839
|
+
.action(async (options) => {
|
|
1840
|
+
const global = resolveGlobal(program);
|
|
1841
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1842
|
+
await runGuarded({
|
|
1843
|
+
id: 'view.update-condition',
|
|
1844
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1845
|
+
endpoint: { method: 'POST', path: '/open_api/view/v1/update_condition_view' },
|
|
1846
|
+
}, global, async (ctx) => ctx.client.view.updateConditionView(body, { auth: ctx.auth }));
|
|
1847
|
+
printOk(Boolean(global.json));
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
function registerMeasure(program) {
|
|
1851
|
+
const measure = program.command('measure').description('度量命令');
|
|
1852
|
+
measure
|
|
1853
|
+
.command('charts')
|
|
1854
|
+
.description('按视图查询图表列表')
|
|
1855
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 MeasureChartsByViewRequest')
|
|
1856
|
+
.action(async (options) => {
|
|
1857
|
+
const global = resolveGlobal(program);
|
|
1858
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1859
|
+
const result = await runGuarded({
|
|
1860
|
+
id: 'measure.charts',
|
|
1861
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1862
|
+
endpoint: { method: 'POST', path: '/open_api/measure/charts' },
|
|
1863
|
+
}, global, async (ctx) => ctx.client.measure.query.listChartsByView(body, { auth: ctx.auth }));
|
|
1864
|
+
printData(result, Boolean(global.json));
|
|
1865
|
+
});
|
|
1866
|
+
measure
|
|
1867
|
+
.command('chart-data')
|
|
1868
|
+
.description('获取图表明细数据')
|
|
1869
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1870
|
+
.requiredOption('--chart-id <chartId>', '图表 ID')
|
|
1871
|
+
.action(async (options) => {
|
|
1872
|
+
const global = resolveGlobal(program);
|
|
1873
|
+
const result = await runGuarded({
|
|
1874
|
+
id: 'measure.chart-data',
|
|
1875
|
+
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
1876
|
+
endpoint: { method: 'GET', path: '/open_api/:project_key/measure/:chart_id' },
|
|
1877
|
+
}, global, async (ctx) => ctx.client.measure.getChartData(options.projectKey, options.chartId, { auth: ctx.auth }));
|
|
1878
|
+
printData(result, Boolean(global.json));
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
function registerTenant(program) {
|
|
1882
|
+
const tenant = program.command('tenant').description('租户命令');
|
|
1883
|
+
tenant
|
|
1884
|
+
.command('info')
|
|
1885
|
+
.description('获取商业租户信息')
|
|
1886
|
+
.requiredOption('--tenant-key <tenantKey>', '租户 key')
|
|
1887
|
+
.action(async (options) => {
|
|
1888
|
+
const global = resolveGlobal(program);
|
|
1889
|
+
const result = await runGuarded({
|
|
1890
|
+
id: 'tenant.info',
|
|
1891
|
+
authPolicy: 'PLUGIN_OPTIONAL_USER_KEY',
|
|
1892
|
+
endpoint: { method: 'GET', path: '/open_api/commercial/tenant' },
|
|
1893
|
+
}, global, async (ctx) => ctx.client.tenant.getInfo(options.tenantKey, { auth: ctx.auth }));
|
|
1894
|
+
printData(result, Boolean(global.json));
|
|
1895
|
+
});
|
|
1896
|
+
tenant
|
|
1897
|
+
.command('entitlement')
|
|
1898
|
+
.description('查询租户权益')
|
|
1899
|
+
.requiredOption('--tenant-key <tenantKey>', '租户 key')
|
|
1900
|
+
.action(async (options) => {
|
|
1901
|
+
const global = resolveGlobal(program);
|
|
1902
|
+
const result = await runGuarded({
|
|
1903
|
+
id: 'tenant.entitlement',
|
|
1904
|
+
authPolicy: 'PLUGIN_OPTIONAL_USER_KEY',
|
|
1905
|
+
endpoint: { method: 'GET', path: '/open_api/commercial/product/tenant_entitlement' },
|
|
1906
|
+
}, global, async (ctx) => ctx.client.tenant.getEntitlement(options.tenantKey, { auth: ctx.auth }));
|
|
1907
|
+
printData(result, Boolean(global.json));
|
|
1908
|
+
});
|
|
1909
|
+
tenant
|
|
1910
|
+
.command('spaces')
|
|
1911
|
+
.description('获取租户安装空间列表')
|
|
1912
|
+
.requiredOption('--tenant-key <tenantKey>', '租户 key')
|
|
1913
|
+
.action(async (options) => {
|
|
1914
|
+
const global = resolveGlobal(program);
|
|
1915
|
+
const result = await runGuarded({
|
|
1916
|
+
id: 'tenant.spaces',
|
|
1917
|
+
authPolicy: 'PLUGIN_OPTIONAL_USER_KEY',
|
|
1918
|
+
endpoint: { method: 'POST', path: '/open_api/tenant/projects/install/list' },
|
|
1919
|
+
}, global, async (ctx) => ctx.client.tenant.listInstalledSpaces(options.tenantKey, { auth: ctx.auth }));
|
|
1920
|
+
printData(result, Boolean(global.json));
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
function registerUser(program) {
|
|
1924
|
+
const user = program.command('user').description('用户相关命令');
|
|
1925
|
+
user
|
|
1926
|
+
.command('query')
|
|
1927
|
+
.description('按 user_key/out_id/email 查询用户详情')
|
|
1928
|
+
.option('--self', '直接查询当前 userKey 对应的用户')
|
|
1929
|
+
.option('--user-keys <userKeys>', '逗号分隔')
|
|
1930
|
+
.option('--out-ids <outIds>', '逗号分隔')
|
|
1931
|
+
.option('--emails <emails>', '逗号分隔')
|
|
1932
|
+
.option('--tenant-key <tenantKey>', '跨租户邮件查询使用')
|
|
1933
|
+
.action(async (options) => {
|
|
1934
|
+
const global = resolveGlobal(program);
|
|
1935
|
+
const result = await runGuarded({
|
|
1936
|
+
id: 'user.query',
|
|
1937
|
+
authPolicy: 'PLUGIN_OPTIONAL_USER_KEY',
|
|
1938
|
+
endpoint: { method: 'POST', path: '/open_api/user/query' },
|
|
1939
|
+
}, global, async (ctx) => {
|
|
1940
|
+
const userKeys = [
|
|
1941
|
+
...(options.self && ctx.userKey ? [ctx.userKey] : []),
|
|
1942
|
+
...parseCsv(options.userKeys),
|
|
1943
|
+
];
|
|
1944
|
+
const outIds = parseCsv(options.outIds);
|
|
1945
|
+
const emails = parseCsv(options.emails);
|
|
1946
|
+
if (userKeys.length === 0 && outIds.length === 0 && emails.length === 0) {
|
|
1947
|
+
throw new CliError('user query 至少需要 --self / --user-keys / --out-ids / --emails 之一', 2);
|
|
1948
|
+
}
|
|
1949
|
+
return ctx.client.user.query({
|
|
1950
|
+
user_keys: userKeys.length > 0 ? userKeys : undefined,
|
|
1951
|
+
out_ids: outIds.length > 0 ? outIds : undefined,
|
|
1952
|
+
emails: emails.length > 0 ? emails : undefined,
|
|
1953
|
+
tenant_key: options.tenantKey,
|
|
1954
|
+
}, { auth: ctx.auth });
|
|
1955
|
+
});
|
|
1956
|
+
printData(result, Boolean(global.json));
|
|
1957
|
+
});
|
|
1958
|
+
user
|
|
1959
|
+
.command('search')
|
|
1960
|
+
.description('搜索租户内用户(仅支持 user_access_token,plugin-only 会拒绝)')
|
|
1961
|
+
.requiredOption('--query <query>', '搜索关键词')
|
|
1962
|
+
.option('--project-key <projectKey>', '空间 key')
|
|
1963
|
+
.action(async (options) => {
|
|
1964
|
+
const global = resolveGlobal(program);
|
|
1965
|
+
const result = await runGuarded({
|
|
1966
|
+
id: 'user.search',
|
|
1967
|
+
authPolicy: 'USER_TOKEN_REQUIRED',
|
|
1968
|
+
endpoint: { method: 'POST', path: '/open_api/user/search' },
|
|
1969
|
+
}, global, async (ctx) => ctx.client.user.search({
|
|
1970
|
+
query: options.query,
|
|
1971
|
+
project_key: options.projectKey,
|
|
1972
|
+
}, { auth: ctx.auth }));
|
|
1973
|
+
printData(result, Boolean(global.json));
|
|
1974
|
+
});
|
|
1975
|
+
const group = user.command('group').description('用户组命令(当前 plugin-only 拒绝)');
|
|
1976
|
+
group
|
|
1977
|
+
.command('create')
|
|
1978
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1979
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 CreateUserGroupRequest')
|
|
1980
|
+
.action(async (options) => {
|
|
1981
|
+
const global = resolveGlobal(program);
|
|
1982
|
+
await runGuarded({
|
|
1983
|
+
id: 'user.group.create',
|
|
1984
|
+
authPolicy: 'USER_TOKEN_REQUIRED',
|
|
1985
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/user_group' },
|
|
1986
|
+
}, global, async (ctx) => {
|
|
1987
|
+
const body = await readJsonFile(options.bodyFile);
|
|
1988
|
+
return ctx.client.user.group.create(options.projectKey, body, { auth: ctx.auth });
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
group
|
|
1992
|
+
.command('update-members')
|
|
1993
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
1994
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 UpdateUserGroupMembersRequest')
|
|
1995
|
+
.action(async (options) => {
|
|
1996
|
+
const global = resolveGlobal(program);
|
|
1997
|
+
await runGuarded({
|
|
1998
|
+
id: 'user.group.update-members',
|
|
1999
|
+
authPolicy: 'USER_TOKEN_REQUIRED',
|
|
2000
|
+
endpoint: { method: 'PATCH', path: '/open_api/:project_key/user_group/members' },
|
|
2001
|
+
}, global, async (ctx) => {
|
|
2002
|
+
const body = await readJsonFile(options.bodyFile);
|
|
2003
|
+
return ctx.client.user.group.updateMembers(options.projectKey, body, { auth: ctx.auth });
|
|
2004
|
+
});
|
|
2005
|
+
});
|
|
2006
|
+
group
|
|
2007
|
+
.command('query-members')
|
|
2008
|
+
.requiredOption('--project-key <projectKey>', '空间 key')
|
|
2009
|
+
.requiredOption('--body-file <path>', 'JSON 文件,内容对应 QueryUserGroupMembersRequest')
|
|
2010
|
+
.action(async (options) => {
|
|
2011
|
+
const global = resolveGlobal(program);
|
|
2012
|
+
await runGuarded({
|
|
2013
|
+
id: 'user.group.query-members',
|
|
2014
|
+
authPolicy: 'USER_TOKEN_REQUIRED',
|
|
2015
|
+
endpoint: { method: 'POST', path: '/open_api/:project_key/user_groups/members/page' },
|
|
2016
|
+
}, global, async (ctx) => {
|
|
2017
|
+
const body = await readJsonFile(options.bodyFile);
|
|
2018
|
+
return ctx.client.user.group.queryMembers(options.projectKey, body, { auth: ctx.auth });
|
|
2019
|
+
});
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
export function buildCli() {
|
|
2023
|
+
const program = new Command();
|
|
2024
|
+
program
|
|
2025
|
+
.name('meegle')
|
|
2026
|
+
.description('Meegle plugin-only CLI (基于 meeglesdk)')
|
|
2027
|
+
.option('--profile <name>', 'profile 名称,默认 active profile')
|
|
2028
|
+
.option('--user-key <userKey>', '本次命令覆盖 userKey')
|
|
2029
|
+
.option('--compat-auth', '读接口使用兼容模式 (x-auth-mode=0),默认严格模式')
|
|
2030
|
+
.option('--json', '输出 JSON')
|
|
2031
|
+
.addHelpText('after', `
|
|
2032
|
+
常用示例:
|
|
2033
|
+
meegle auth init --target-profile demo --plugin-id <id> --plugin-secret <secret> --default-user-key <userKey>
|
|
2034
|
+
meegle --profile demo auth status
|
|
2035
|
+
meegle --profile demo space list
|
|
2036
|
+
meegle --profile demo user query --self
|
|
2037
|
+
meegle --profile demo workitem get --project-key <projectKey> --type story --id 6300034462
|
|
2038
|
+
meegle --profile demo workitem create --project-key <projectKey> --type story --name "登录优化"
|
|
2039
|
+
meegle --profile demo comment add --project-key <projectKey> --type story --id 6300034462 --content "收到"
|
|
2040
|
+
meegle --profile demo subtask create --project-key <projectKey> --type story --id 6300034462 --node-id doing --name "需求分析"
|
|
2041
|
+
meegle --profile demo workflow state-change --project-key <projectKey> --type story --id 6300034462 --transition-id 12345
|
|
2042
|
+
`);
|
|
2043
|
+
registerAuth(program);
|
|
2044
|
+
registerSpace(program);
|
|
2045
|
+
registerWorkItem(program);
|
|
2046
|
+
registerWorkflow(program);
|
|
2047
|
+
registerComment(program);
|
|
2048
|
+
registerSubtask(program);
|
|
2049
|
+
registerAttachment(program);
|
|
2050
|
+
registerWorkHour(program);
|
|
2051
|
+
registerView(program);
|
|
2052
|
+
registerMeasure(program);
|
|
2053
|
+
registerTenant(program);
|
|
2054
|
+
registerUser(program);
|
|
2055
|
+
return program;
|
|
2056
|
+
}
|