meegle-cli 0.1.2 → 0.1.4
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-en.md +29 -0
- package/README.md +76 -6
- package/dist/cli.js +48 -3
- package/dist/core/auth-policy.d.ts +5 -0
- package/dist/core/auth-policy.js +43 -2
- package/dist/core/cli-error.d.ts +8 -1
- package/dist/core/cli-error.js +9 -2
- package/dist/core/command-guard.d.ts +11 -1
- package/dist/core/command-guard.js +123 -3
- package/dist/core/error-handler.js +84 -13
- package/package.json +3 -2
- package/skills/meegle-cli-usage/SKILL.md +108 -0
- package/skills/meegle-cli-usage/agents/openai.yaml +4 -0
- package/skills/meegle-cli-usage/references/command-recipes.md +99 -0
package/README-en.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
CLI for Feishu Project (Meegle).
|
|
4
4
|
Current mode is `plugin-only`: it supports `plugin_access_token / virtual_plugin_token`, but not `user_access_token`.
|
|
5
5
|
|
|
6
|
+
## Risk Notes
|
|
7
|
+
|
|
8
|
+
This CLI can directly create, update, and delete Meegle data.
|
|
9
|
+
In a B2B environment, the main risks are:
|
|
10
|
+
|
|
11
|
+
- using the wrong `profile`
|
|
12
|
+
- using the wrong `projectKey`
|
|
13
|
+
- letting scripts or AI tools write to production by mistake
|
|
14
|
+
- running bulk mutations without checking the target scope first
|
|
15
|
+
|
|
16
|
+
Starting from `0.1.3`:
|
|
17
|
+
|
|
18
|
+
- write commands print a risk summary first
|
|
19
|
+
- interactive terminals require confirmation
|
|
20
|
+
- non-interactive environments (scripts / AI / CI) must pass `--yes`
|
|
21
|
+
|
|
6
22
|
## Install
|
|
7
23
|
|
|
8
24
|
```bash
|
|
@@ -78,6 +94,7 @@ meegle --profile default workitem get \
|
|
|
78
94
|
|
|
79
95
|
```bash
|
|
80
96
|
meegle --profile default workitem create \
|
|
97
|
+
--yes \
|
|
81
98
|
--project-key <PROJECT_KEY> \
|
|
82
99
|
--type story \
|
|
83
100
|
--name "Login improvement" \
|
|
@@ -91,6 +108,7 @@ meegle --profile default workitem create \
|
|
|
91
108
|
|
|
92
109
|
```bash
|
|
93
110
|
meegle --profile default comment add \
|
|
111
|
+
--yes \
|
|
94
112
|
--project-key <PROJECT_KEY> \
|
|
95
113
|
--type story \
|
|
96
114
|
--id 6300034462 \
|
|
@@ -165,6 +183,16 @@ meegle --profile default workitem search by-params \
|
|
|
165
183
|
|
|
166
184
|
Replace `1234567890` in the example file with the real version work item ID.
|
|
167
185
|
|
|
186
|
+
## Write Safety
|
|
187
|
+
|
|
188
|
+
In non-interactive environments, write commands must pass `--yes`.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
meegle --profile default --yes workitem update ...
|
|
194
|
+
```
|
|
195
|
+
|
|
168
196
|
## Agent Notes
|
|
169
197
|
|
|
170
198
|
If you use this CLI from an AI agent or automation:
|
|
@@ -173,6 +201,7 @@ If you use this CLI from an AI agent or automation:
|
|
|
173
201
|
2. do not guess request shapes from web URLs
|
|
174
202
|
3. use `workitem search by-params` for custom fields and related fields
|
|
175
203
|
4. `field_value_pairs` is for create/update, not for search
|
|
204
|
+
5. in non-interactive environments, write commands must pass `--yes`
|
|
176
205
|
|
|
177
206
|
## Examples
|
|
178
207
|
|
package/README.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
3
3
|
面向飞书项目(Meegle)的命令行工具。
|
|
4
4
|
当前版本运行在 `plugin-only` 模式:只支持 `plugin_access_token / virtual_plugin_token`,不支持 `user_access_token`。
|
|
5
5
|
|
|
6
|
+
## 风险提示
|
|
7
|
+
|
|
8
|
+
这是一个可直接对空间数据执行增删改的 CLI。
|
|
9
|
+
在 toB 场景里,误操作成本很高,尤其是:
|
|
10
|
+
|
|
11
|
+
- 打错 `profile`
|
|
12
|
+
- 打错 `projectKey`
|
|
13
|
+
- 把脚本或 AI 的写操作直接打到线上
|
|
14
|
+
- 批量更新 / 删除目标超出预期
|
|
15
|
+
|
|
16
|
+
从 `0.1.3` 起:
|
|
17
|
+
|
|
18
|
+
- 写操作会先输出风险摘要
|
|
19
|
+
- 交互终端里默认要求确认
|
|
20
|
+
- 非交互环境(脚本 / AI / CI)必须显式传 `--yes`
|
|
21
|
+
- 批量目标超过安全阈值时,必须显式传 `--force-batch`
|
|
22
|
+
|
|
23
|
+
交互终端的目标体验是:
|
|
24
|
+
|
|
25
|
+
- 人类用户只需要回复简短确认词,例如 `确认删除`
|
|
26
|
+
- Agent / 脚本才需要显式 `--yes`
|
|
27
|
+
|
|
28
|
+
建议:
|
|
29
|
+
|
|
30
|
+
1. 先在测试空间验证命令
|
|
31
|
+
2. 再切生产 profile
|
|
32
|
+
3. 对写操作显式确认 `profile / projectKey / id`
|
|
33
|
+
|
|
6
34
|
## 适用对象
|
|
7
35
|
|
|
8
36
|
- 想在终端里查询空间、工作项、视图、评论、子任务
|
|
@@ -159,6 +187,7 @@ meegle --profile default workitem get \
|
|
|
159
187
|
|
|
160
188
|
```bash
|
|
161
189
|
meegle --profile default workitem create \
|
|
190
|
+
--yes \
|
|
162
191
|
--project-key <PROJECT_KEY> \
|
|
163
192
|
--type story \
|
|
164
193
|
--name "登录优化" \
|
|
@@ -172,6 +201,7 @@ meegle --profile default workitem create \
|
|
|
172
201
|
|
|
173
202
|
```bash
|
|
174
203
|
meegle --profile default workitem update \
|
|
204
|
+
--yes \
|
|
175
205
|
--project-key <PROJECT_KEY> \
|
|
176
206
|
--type story \
|
|
177
207
|
--id 6300034462 \
|
|
@@ -183,6 +213,7 @@ meegle --profile default workitem update \
|
|
|
183
213
|
|
|
184
214
|
```bash
|
|
185
215
|
meegle --profile default comment add \
|
|
216
|
+
--yes \
|
|
186
217
|
--project-key <PROJECT_KEY> \
|
|
187
218
|
--type story \
|
|
188
219
|
--id 6300034462 \
|
|
@@ -194,6 +225,7 @@ meegle --profile default comment add \
|
|
|
194
225
|
|
|
195
226
|
```bash
|
|
196
227
|
meegle --profile default comment add \
|
|
228
|
+
--yes \
|
|
197
229
|
--project-key <PROJECT_KEY> \
|
|
198
230
|
--type story \
|
|
199
231
|
--id 6300034462 \
|
|
@@ -205,6 +237,7 @@ meegle --profile default comment add \
|
|
|
205
237
|
|
|
206
238
|
```bash
|
|
207
239
|
meegle --profile default subtask create \
|
|
240
|
+
--yes \
|
|
208
241
|
--project-key <PROJECT_KEY> \
|
|
209
242
|
--type story \
|
|
210
243
|
--id 6300034462 \
|
|
@@ -222,6 +255,7 @@ meegle --profile default subtask create \
|
|
|
222
255
|
|
|
223
256
|
```bash
|
|
224
257
|
meegle --profile default workflow state-change \
|
|
258
|
+
--yes \
|
|
225
259
|
--project-key <PROJECT_KEY> \
|
|
226
260
|
--type story \
|
|
227
261
|
--id 6300034462 \
|
|
@@ -234,6 +268,7 @@ meegle --profile default workflow state-change \
|
|
|
234
268
|
|
|
235
269
|
```bash
|
|
236
270
|
meegle --profile default workflow node-operate \
|
|
271
|
+
--yes \
|
|
237
272
|
--project-key <PROJECT_KEY> \
|
|
238
273
|
--type story \
|
|
239
274
|
--id 6300034462 \
|
|
@@ -377,7 +412,7 @@ meegle --profile default workitem search filter \
|
|
|
377
412
|
|
|
378
413
|
示例文件:
|
|
379
414
|
|
|
380
|
-
- [examples/version-search-filter.json](
|
|
415
|
+
- [examples/version-search-filter.json](./examples/version-search-filter.json)
|
|
381
416
|
|
|
382
417
|
你要从返回结果里拿到真正的版本实例 `id`。
|
|
383
418
|
|
|
@@ -393,7 +428,7 @@ meegle --profile default workitem search by-params \
|
|
|
393
428
|
|
|
394
429
|
示例文件:
|
|
395
430
|
|
|
396
|
-
- [examples/story-by-planning-version.json](
|
|
431
|
+
- [examples/story-by-planning-version.json](./examples/story-by-planning-version.json)
|
|
397
432
|
|
|
398
433
|
重点:
|
|
399
434
|
|
|
@@ -420,6 +455,8 @@ meegle --profile default workitem search by-params \
|
|
|
420
455
|
|
|
421
456
|
- 常用查询优先走直接参数,不强制写 JSON 文件
|
|
422
457
|
- 常用写操作优先走直接参数,例如 `--name`、`--desc`、`--content`
|
|
458
|
+
- 非交互环境下,写操作必须显式传 `--yes`
|
|
459
|
+
- 超大批量写操作必须显式传 `--force-batch`
|
|
423
460
|
- 复杂请求体可使用 `--body <json>` 或 `--body-file <path>`
|
|
424
461
|
- 复杂查询参数可使用 `--query-file <path>`
|
|
425
462
|
- 自定义字段统一使用 `--field field_key=value`
|
|
@@ -497,6 +534,36 @@ meegle auth init \
|
|
|
497
534
|
3. 请求体键是不是对
|
|
498
535
|
- `field_value_pairs` 用于创建 / 更新,不是搜索条件
|
|
499
536
|
|
|
537
|
+
### 6. 写操作在脚本 / Agent 里被拒绝
|
|
538
|
+
|
|
539
|
+
如果报错类似:
|
|
540
|
+
|
|
541
|
+
- `非交互环境下,写操作需要显式传 --yes`
|
|
542
|
+
|
|
543
|
+
说明当前进程不是交互终端。
|
|
544
|
+
这是预期行为,用来拦截脚本和 AI 的误操作。
|
|
545
|
+
|
|
546
|
+
处理方式:
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
meegle --profile default --yes workitem update ...
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### 7. 批量写操作被拒绝
|
|
553
|
+
|
|
554
|
+
如果报错类似:
|
|
555
|
+
|
|
556
|
+
- `BATCH_THRESHOLD_EXCEEDED`
|
|
557
|
+
|
|
558
|
+
说明目标数量已经超过安全阈值。
|
|
559
|
+
这类场景默认拒绝,避免一次误伤太多数据。
|
|
560
|
+
|
|
561
|
+
如果确实要继续,显式传:
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
meegle --profile default --yes --force-batch workitem freeze --ids ...
|
|
565
|
+
```
|
|
566
|
+
|
|
500
567
|
## 命令总览
|
|
501
568
|
|
|
502
569
|
- `auth`: `init`, `status`
|
|
@@ -530,10 +597,10 @@ npm test
|
|
|
530
597
|
|
|
531
598
|
可直接复制修改的模板在:
|
|
532
599
|
|
|
533
|
-
- [examples/view-list.json](
|
|
534
|
-
- [examples/view-fix-items.json](
|
|
535
|
-
- [examples/version-search-filter.json](
|
|
536
|
-
- [examples/story-by-planning-version.json](
|
|
600
|
+
- [examples/view-list.json](./examples/view-list.json)
|
|
601
|
+
- [examples/view-fix-items.json](./examples/view-fix-items.json)
|
|
602
|
+
- [examples/version-search-filter.json](./examples/version-search-filter.json)
|
|
603
|
+
- [examples/story-by-planning-version.json](./examples/story-by-planning-version.json)
|
|
537
604
|
|
|
538
605
|
## AI / Agent 提示
|
|
539
606
|
|
|
@@ -544,3 +611,6 @@ npm test
|
|
|
544
611
|
3. 自定义字段和关联字段优先走 `workitem search by-params`
|
|
545
612
|
4. `planning_version` 这类关联字段,先查出被关联工作项的 `id`,再筛主工作项
|
|
546
613
|
5. `field_value_pairs` 是写操作格式,不是搜索格式
|
|
614
|
+
6. 非交互环境里,写操作必须显式传 `--yes`
|
|
615
|
+
7. 对批量危险操作,超过阈值时必须显式传 `--force-batch`
|
|
616
|
+
8. 遇到安全错误时,优先读取 `error.code`,不要只匹配自然语言
|
package/dist/cli.js
CHANGED
|
@@ -708,6 +708,41 @@ async function buildWorkflowNodeOperateRequest(options) {
|
|
|
708
708
|
fields: fields.length > 0 ? fields : undefined,
|
|
709
709
|
};
|
|
710
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* 删除前先读出目标摘要,能明显降低误删概率。
|
|
713
|
+
*
|
|
714
|
+
* @param ctx 守卫上下文
|
|
715
|
+
* @param args 工作项定位参数
|
|
716
|
+
*/
|
|
717
|
+
async function buildWorkItemRemovePreview(ctx, args) {
|
|
718
|
+
const workItemId = parseInteger(args.id, 'id');
|
|
719
|
+
const items = await ctx.client.workItem.query(args.projectKey, args.type, {
|
|
720
|
+
work_item_ids: [workItemId],
|
|
721
|
+
fields: ['name'],
|
|
722
|
+
}, { auth: ctx.auth });
|
|
723
|
+
const target = items[0];
|
|
724
|
+
if (!target) {
|
|
725
|
+
throw new CliError('删除前未找到目标工作项,请先确认 projectKey、type 和 id 是否正确。', 2);
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
lines: [
|
|
729
|
+
`projectKey: ${args.projectKey}`,
|
|
730
|
+
`workItemType: ${args.type}`,
|
|
731
|
+
`workItemId: ${workItemId}`,
|
|
732
|
+
`workItemName: ${target.name}`,
|
|
733
|
+
],
|
|
734
|
+
confirmPhrase: '确认删除',
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function buildBatchIdsPreview(action, ids) {
|
|
738
|
+
return {
|
|
739
|
+
lines: [
|
|
740
|
+
`action: ${action}`,
|
|
741
|
+
`targetIds: ${ids.join(',')}`,
|
|
742
|
+
],
|
|
743
|
+
targetCount: ids.length,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
711
746
|
function resolveGlobal(program) {
|
|
712
747
|
return program.opts();
|
|
713
748
|
}
|
|
@@ -960,7 +995,9 @@ function registerWorkItem(program) {
|
|
|
960
995
|
id: 'workitem.remove',
|
|
961
996
|
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
962
997
|
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 })
|
|
998
|
+
}, global, async (ctx) => ctx.client.workItem.remove(options.projectKey, options.type, parseInteger(options.id, 'id'), { auth: ctx.auth }), {
|
|
999
|
+
riskPreview: async (ctx) => buildWorkItemRemovePreview(ctx, options),
|
|
1000
|
+
});
|
|
964
1001
|
printOk(Boolean(global.json));
|
|
965
1002
|
});
|
|
966
1003
|
workItem
|
|
@@ -969,11 +1006,14 @@ function registerWorkItem(program) {
|
|
|
969
1006
|
.requiredOption('--ids <ids>', '工作项 ID,逗号分隔')
|
|
970
1007
|
.action(async (options) => {
|
|
971
1008
|
const global = resolveGlobal(program);
|
|
1009
|
+
const ids = parseNumberList(options.ids, 'ids');
|
|
972
1010
|
await runGuarded({
|
|
973
1011
|
id: 'workitem.freeze',
|
|
974
1012
|
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
975
1013
|
endpoint: { method: 'PUT', path: '/open_api/work_item/freeze' },
|
|
976
|
-
}, global, async (ctx) => ctx.client.workItem.freeze(
|
|
1014
|
+
}, global, async (ctx) => ctx.client.workItem.freeze(ids, true, { auth: ctx.auth }), {
|
|
1015
|
+
riskPreview: async () => buildBatchIdsPreview('freeze', ids),
|
|
1016
|
+
});
|
|
977
1017
|
printOk(Boolean(global.json));
|
|
978
1018
|
});
|
|
979
1019
|
workItem
|
|
@@ -982,11 +1022,14 @@ function registerWorkItem(program) {
|
|
|
982
1022
|
.requiredOption('--ids <ids>', '工作项 ID,逗号分隔')
|
|
983
1023
|
.action(async (options) => {
|
|
984
1024
|
const global = resolveGlobal(program);
|
|
1025
|
+
const ids = parseNumberList(options.ids, 'ids');
|
|
985
1026
|
await runGuarded({
|
|
986
1027
|
id: 'workitem.unfreeze',
|
|
987
1028
|
authPolicy: 'PLUGIN_WITH_USER_KEY',
|
|
988
1029
|
endpoint: { method: 'PUT', path: '/open_api/work_item/freeze' },
|
|
989
|
-
}, global, async (ctx) => ctx.client.workItem.freeze(
|
|
1030
|
+
}, global, async (ctx) => ctx.client.workItem.freeze(ids, false, { auth: ctx.auth }), {
|
|
1031
|
+
riskPreview: async () => buildBatchIdsPreview('unfreeze', ids),
|
|
1032
|
+
});
|
|
990
1033
|
printOk(Boolean(global.json));
|
|
991
1034
|
});
|
|
992
1035
|
workItem
|
|
@@ -2028,6 +2071,8 @@ export function buildCli() {
|
|
|
2028
2071
|
.option('--user-key <userKey>', '本次命令覆盖 userKey')
|
|
2029
2072
|
.option('--compat-auth', '读接口使用兼容模式 (x-auth-mode=0),默认严格模式')
|
|
2030
2073
|
.option('--json', '输出 JSON')
|
|
2074
|
+
.option('--yes', '跳过写操作确认,脚本 / AI / CI 使用')
|
|
2075
|
+
.option('--force-batch', '批量目标超过安全阈值时,显式确认继续执行')
|
|
2031
2076
|
.addHelpText('after', `
|
|
2032
2077
|
常用示例:
|
|
2033
2078
|
meegle auth init --target-profile demo --plugin-id <id> --plugin-secret <secret> --default-user-key <userKey>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
2
2
|
export type AuthPolicy = 'NONE' | 'PLUGIN_WITH_USER_KEY' | 'PLUGIN_OPTIONAL_USER_KEY' | 'USER_TOKEN_REQUIRED';
|
|
3
|
+
export type RiskLevel = 'READ_ONLY' | 'WRITE' | 'DANGEROUS';
|
|
3
4
|
export interface EndpointRef {
|
|
4
5
|
method: HttpMethod;
|
|
5
6
|
path: string;
|
|
@@ -12,3 +13,7 @@ export interface CommandMeta {
|
|
|
12
13
|
export declare function isUserTokenOnlyEndpoint(path: string): boolean;
|
|
13
14
|
export declare function assertCommandSupported(meta: CommandMeta): void;
|
|
14
15
|
export declare function assertEndpointAllowed(endpoint?: EndpointRef): void;
|
|
16
|
+
/**
|
|
17
|
+
* 风险级别由命令语义决定,比按 HTTP 方法推断更稳定。
|
|
18
|
+
*/
|
|
19
|
+
export declare function getCommandRiskLevel(meta: CommandMeta): RiskLevel;
|
package/dist/core/auth-policy.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { CliError } from './cli-error.js';
|
|
2
|
+
const DANGEROUS_COMMAND_PATTERNS = [
|
|
3
|
+
/^workitem\.(remove|freeze|unfreeze|abort|restore|update-compound)$/,
|
|
4
|
+
/^view\.delete$/,
|
|
5
|
+
/^attachment\.delete$/,
|
|
6
|
+
/^workhour\.delete$/,
|
|
7
|
+
];
|
|
8
|
+
const WRITE_COMMAND_PATTERNS = [
|
|
9
|
+
/^workitem\.create$/,
|
|
10
|
+
/^workitem\.update$/,
|
|
11
|
+
/^workflow\.(state-change|node-operate|node-update)$/,
|
|
12
|
+
/^comment\.(add|update|remove)$/,
|
|
13
|
+
/^subtask\.(create|update|remove|operate)$/,
|
|
14
|
+
/^attachment\.(upload-file|upload)$/,
|
|
15
|
+
/^workhour\.(create|update)$/,
|
|
16
|
+
/^view\.(create-fix|update-fix|create-condition|update-condition)$/,
|
|
17
|
+
];
|
|
2
18
|
const USER_TOKEN_ONLY_TEMPLATES = new Set([
|
|
3
19
|
'/open_api/user/search',
|
|
4
20
|
'/open_api/:project_key/user_group',
|
|
@@ -24,7 +40,13 @@ export function isUserTokenOnlyEndpoint(path) {
|
|
|
24
40
|
}
|
|
25
41
|
export function assertCommandSupported(meta) {
|
|
26
42
|
if (meta.authPolicy === 'USER_TOKEN_REQUIRED') {
|
|
27
|
-
throw new CliError(`命令 "${meta.id}" 依赖 user_access_token,当前 CLI 运行在 plugin-only 模式,已拒绝执行。`,
|
|
43
|
+
throw new CliError(`命令 "${meta.id}" 依赖 user_access_token,当前 CLI 运行在 plugin-only 模式,已拒绝执行。`, {
|
|
44
|
+
exitCode: 2,
|
|
45
|
+
code: 'USER_TOKEN_REQUIRED',
|
|
46
|
+
details: {
|
|
47
|
+
command: meta.id,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
52
|
export function assertEndpointAllowed(endpoint) {
|
|
@@ -32,6 +54,25 @@ export function assertEndpointAllowed(endpoint) {
|
|
|
32
54
|
return;
|
|
33
55
|
}
|
|
34
56
|
if (isUserTokenOnlyEndpoint(endpoint.path)) {
|
|
35
|
-
throw new CliError(`接口 ${endpoint.method} ${endpoint.path} 在文档中标注为仅支持 user_access_token,plugin-only 模式已拒绝。`,
|
|
57
|
+
throw new CliError(`接口 ${endpoint.method} ${endpoint.path} 在文档中标注为仅支持 user_access_token,plugin-only 模式已拒绝。`, {
|
|
58
|
+
exitCode: 2,
|
|
59
|
+
code: 'ENDPOINT_REQUIRES_USER_TOKEN',
|
|
60
|
+
details: {
|
|
61
|
+
method: endpoint.method,
|
|
62
|
+
path: endpoint.path,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 风险级别由命令语义决定,比按 HTTP 方法推断更稳定。
|
|
69
|
+
*/
|
|
70
|
+
export function getCommandRiskLevel(meta) {
|
|
71
|
+
if (DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(meta.id))) {
|
|
72
|
+
return 'DANGEROUS';
|
|
73
|
+
}
|
|
74
|
+
if (WRITE_COMMAND_PATTERNS.some((pattern) => pattern.test(meta.id))) {
|
|
75
|
+
return 'WRITE';
|
|
36
76
|
}
|
|
77
|
+
return 'READ_ONLY';
|
|
37
78
|
}
|
package/dist/core/cli-error.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
export interface CliErrorOptions {
|
|
2
|
+
exitCode?: number;
|
|
3
|
+
code?: string;
|
|
4
|
+
details?: Record<string, unknown>;
|
|
5
|
+
}
|
|
1
6
|
export declare class CliError extends Error {
|
|
2
7
|
readonly exitCode: number;
|
|
3
|
-
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly details?: Record<string, unknown>;
|
|
10
|
+
constructor(message: string, exitCodeOrOptions?: number | CliErrorOptions);
|
|
4
11
|
}
|
package/dist/core/cli-error.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export class CliError extends Error {
|
|
2
2
|
exitCode;
|
|
3
|
-
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(message, exitCodeOrOptions = 1) {
|
|
4
6
|
super(message);
|
|
5
7
|
this.name = 'CliError';
|
|
6
|
-
|
|
8
|
+
const options = typeof exitCodeOrOptions === 'number'
|
|
9
|
+
? { exitCode: exitCodeOrOptions }
|
|
10
|
+
: exitCodeOrOptions;
|
|
11
|
+
this.exitCode = options.exitCode ?? 1;
|
|
12
|
+
this.code = options.code ?? 'CLI_ERROR';
|
|
13
|
+
this.details = options.details;
|
|
7
14
|
Object.setPrototypeOf(this, CliError.prototype);
|
|
8
15
|
}
|
|
9
16
|
}
|
|
@@ -5,6 +5,8 @@ export interface RuntimeOptions {
|
|
|
5
5
|
profile?: string;
|
|
6
6
|
userKey?: string;
|
|
7
7
|
compatAuth?: boolean;
|
|
8
|
+
yes?: boolean;
|
|
9
|
+
forceBatch?: boolean;
|
|
8
10
|
}
|
|
9
11
|
export interface GuardContext {
|
|
10
12
|
profileName: string;
|
|
@@ -13,4 +15,12 @@ export interface GuardContext {
|
|
|
13
15
|
auth: AuthContext;
|
|
14
16
|
userKey?: string;
|
|
15
17
|
}
|
|
16
|
-
export
|
|
18
|
+
export interface RiskPreview {
|
|
19
|
+
lines?: string[];
|
|
20
|
+
confirmPhrase?: string;
|
|
21
|
+
targetCount?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface GuardHooks {
|
|
24
|
+
riskPreview?: (ctx: GuardContext) => Promise<RiskPreview | undefined>;
|
|
25
|
+
}
|
|
26
|
+
export declare function runGuarded<T>(meta: CommandMeta, runtime: RuntimeOptions, action: (ctx: GuardContext) => Promise<T>, hooks?: GuardHooks): Promise<T>;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
1
2
|
import { CliError } from './cli-error.js';
|
|
2
|
-
import { assertCommandSupported, assertEndpointAllowed } from './auth-policy.js';
|
|
3
|
+
import { assertCommandSupported, assertEndpointAllowed, getCommandRiskLevel, } from './auth-policy.js';
|
|
3
4
|
import { getProfile } from './config-store.js';
|
|
4
5
|
import { createClient } from './client-factory.js';
|
|
6
|
+
const BATCH_WARNING_THRESHOLD = 20;
|
|
7
|
+
const BATCH_FORCE_THRESHOLD = 100;
|
|
5
8
|
function resolveUserKey(runtimeUserKey, defaultUserKey) {
|
|
6
9
|
if (runtimeUserKey?.trim()) {
|
|
7
10
|
return runtimeUserKey.trim();
|
|
@@ -24,7 +27,121 @@ function buildPluginAuth(userKey, compatAuth) {
|
|
|
24
27
|
authMode: compatAuth ? 0 : 1,
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
|
-
|
|
30
|
+
function isInteractiveShell() {
|
|
31
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
32
|
+
}
|
|
33
|
+
function isHighRiskProfile(profileName) {
|
|
34
|
+
return /(^|[-_])(prod|production|live)([-_]|$)/i.test(profileName);
|
|
35
|
+
}
|
|
36
|
+
function escalateRiskLevel(riskLevel, profileName) {
|
|
37
|
+
if (!isHighRiskProfile(profileName)) {
|
|
38
|
+
return riskLevel;
|
|
39
|
+
}
|
|
40
|
+
if (riskLevel === 'WRITE') {
|
|
41
|
+
return 'DANGEROUS';
|
|
42
|
+
}
|
|
43
|
+
return riskLevel;
|
|
44
|
+
}
|
|
45
|
+
function buildRiskSummary(meta, profileName, riskLevel) {
|
|
46
|
+
const args = process.argv.slice(2).join(' ');
|
|
47
|
+
return [
|
|
48
|
+
`命令: meegle ${args}`,
|
|
49
|
+
`profile: ${profileName}`,
|
|
50
|
+
`风险级别: ${riskLevel}`,
|
|
51
|
+
meta.endpoint ? `接口: ${meta.endpoint.method} ${meta.endpoint.path}` : '接口: N/A',
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 在统一守卫里注入风险预览,避免每个高风险命令各自实现一套确认流程。
|
|
56
|
+
*
|
|
57
|
+
* @param meta 命令元数据
|
|
58
|
+
* @param runtime 运行时选项
|
|
59
|
+
* @param profileName 当前 profile 名称
|
|
60
|
+
* @param preview 命令特定的风险预览
|
|
61
|
+
*/
|
|
62
|
+
async function confirmWriteRisk(meta, runtime, profileName, preview) {
|
|
63
|
+
const originalRisk = getCommandRiskLevel(meta);
|
|
64
|
+
const riskLevel = escalateRiskLevel(originalRisk, profileName);
|
|
65
|
+
if (riskLevel === 'READ_ONLY') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const summary = [...buildRiskSummary(meta, profileName, riskLevel), ...(preview?.lines ?? [])];
|
|
69
|
+
const targetCount = preview?.targetCount;
|
|
70
|
+
if (typeof targetCount === 'number') {
|
|
71
|
+
summary.push(`targetCount: ${targetCount}`);
|
|
72
|
+
if (targetCount > BATCH_WARNING_THRESHOLD) {
|
|
73
|
+
summary.push(`批量目标超过 ${BATCH_WARNING_THRESHOLD} 条,请再次确认范围。`);
|
|
74
|
+
}
|
|
75
|
+
if (targetCount > BATCH_FORCE_THRESHOLD && !runtime.forceBatch) {
|
|
76
|
+
throw new CliError(`批量目标达到 ${targetCount} 条,已超过安全阈值 ${BATCH_FORCE_THRESHOLD}。如确认继续,请显式传 --force-batch。`, {
|
|
77
|
+
exitCode: 2,
|
|
78
|
+
code: 'BATCH_THRESHOLD_EXCEEDED',
|
|
79
|
+
details: {
|
|
80
|
+
target_count: targetCount,
|
|
81
|
+
threshold: BATCH_FORCE_THRESHOLD,
|
|
82
|
+
command: meta.id,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!isInteractiveShell()) {
|
|
88
|
+
if (runtime.yes) {
|
|
89
|
+
process.stderr.write('即将执行写操作:\n');
|
|
90
|
+
for (const line of summary) {
|
|
91
|
+
process.stderr.write(`- ${line}\n`);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw new CliError('非交互环境下,写操作需要显式传 --yes。', {
|
|
96
|
+
exitCode: 2,
|
|
97
|
+
code: 'WRITE_CONFIRMATION_REQUIRED',
|
|
98
|
+
details: {
|
|
99
|
+
command: meta.id,
|
|
100
|
+
risk_level: riskLevel,
|
|
101
|
+
risk_summary: summary,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
process.stderr.write('即将执行写操作:\n');
|
|
106
|
+
for (const line of summary) {
|
|
107
|
+
process.stderr.write(`- ${line}\n`);
|
|
108
|
+
}
|
|
109
|
+
if (runtime.yes) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const rl = createInterface({
|
|
113
|
+
input: process.stdin,
|
|
114
|
+
output: process.stderr,
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
process.stderr.write('即将执行写操作:\n');
|
|
118
|
+
for (const line of summary) {
|
|
119
|
+
process.stderr.write(`- ${line}\n`);
|
|
120
|
+
}
|
|
121
|
+
if (riskLevel === 'DANGEROUS') {
|
|
122
|
+
const confirmPhrase = preview?.confirmPhrase ?? '确认执行';
|
|
123
|
+
const answer = (await rl.question(`危险操作。请输入“${confirmPhrase}”确认继续执行: `)).trim();
|
|
124
|
+
if (answer !== confirmPhrase) {
|
|
125
|
+
throw new CliError('已取消执行。', {
|
|
126
|
+
exitCode: 2,
|
|
127
|
+
code: 'OPERATION_CANCELLED',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const answer = (await rl.question('确认继续执行写操作?[y/N] ')).trim().toLowerCase();
|
|
133
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
134
|
+
throw new CliError('已取消执行。', {
|
|
135
|
+
exitCode: 2,
|
|
136
|
+
code: 'OPERATION_CANCELLED',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
rl.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function runGuarded(meta, runtime, action, hooks) {
|
|
28
145
|
assertCommandSupported(meta);
|
|
29
146
|
assertEndpointAllowed(meta.endpoint);
|
|
30
147
|
const { name: profileName, profile } = await getProfile(runtime.profile);
|
|
@@ -34,5 +151,8 @@ export async function runGuarded(meta, runtime, action) {
|
|
|
34
151
|
}
|
|
35
152
|
const client = createClient(profile);
|
|
36
153
|
const auth = buildPluginAuth(userKey, runtime.compatAuth);
|
|
37
|
-
|
|
154
|
+
const ctx = { profileName, profile, client, auth, userKey };
|
|
155
|
+
const preview = hooks?.riskPreview ? await hooks.riskPreview(ctx) : undefined;
|
|
156
|
+
await confirmWriteRisk(meta, runtime, profileName, preview);
|
|
157
|
+
return action(ctx);
|
|
38
158
|
}
|
|
@@ -2,51 +2,122 @@ import { CommanderError } from 'commander';
|
|
|
2
2
|
import { ErrorCodes, MeegoError } from 'meeglesdk';
|
|
3
3
|
import { CliError } from './cli-error.js';
|
|
4
4
|
const TOKEN_TYPE_WRONG = 1000052755;
|
|
5
|
-
function
|
|
5
|
+
function isJsonMode() {
|
|
6
|
+
return process.argv.includes('--json');
|
|
7
|
+
}
|
|
8
|
+
function printJson(payload) {
|
|
9
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
10
|
+
}
|
|
11
|
+
function printText(message) {
|
|
6
12
|
process.stderr.write(`${message}\n`);
|
|
7
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* 让 Agent 能稳定识别错误类型,比只输出自然语言更容易做重试和纠偏。
|
|
16
|
+
*/
|
|
17
|
+
function printStructuredError(payload) {
|
|
18
|
+
if (isJsonMode()) {
|
|
19
|
+
printJson({
|
|
20
|
+
ok: false,
|
|
21
|
+
error: {
|
|
22
|
+
code: payload.code,
|
|
23
|
+
message: payload.message,
|
|
24
|
+
...(payload.details ?? {}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
printText(`[${payload.code}] ${payload.message}`);
|
|
30
|
+
if (payload.details?.log_id) {
|
|
31
|
+
printText(`log_id: ${String(payload.details.log_id)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
8
34
|
function handleMeegoError(error) {
|
|
9
35
|
switch (error.errCode) {
|
|
10
36
|
case TOKEN_TYPE_WRONG:
|
|
11
|
-
|
|
37
|
+
printStructuredError({
|
|
38
|
+
code: 'USER_TOKEN_REQUIRED',
|
|
39
|
+
message: '接口仅支持 user_access_token;当前 CLI 为 plugin-only 模式,命令已拒绝。',
|
|
40
|
+
details: {
|
|
41
|
+
err_code: error.errCode,
|
|
42
|
+
log_id: error.logId,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
12
45
|
return 2;
|
|
13
46
|
case ErrorCodes.PLUGIN_TOKEN_MUST_HAVE_USER_KEY:
|
|
14
|
-
|
|
47
|
+
printStructuredError({
|
|
48
|
+
code: 'USER_KEY_REQUIRED',
|
|
49
|
+
message: '缺少 userKey。请传 --user-key 或在 auth init 中配置 defaultUserKey。',
|
|
50
|
+
details: {
|
|
51
|
+
err_code: error.errCode,
|
|
52
|
+
log_id: error.logId,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
15
55
|
return 2;
|
|
16
56
|
case ErrorCodes.X_USER_KEY_WRONG:
|
|
17
|
-
|
|
57
|
+
printStructuredError({
|
|
58
|
+
code: 'INVALID_USER_KEY',
|
|
59
|
+
message: 'X-User-Key 无效。请检查 --user-key 是否正确。',
|
|
60
|
+
details: {
|
|
61
|
+
err_code: error.errCode,
|
|
62
|
+
log_id: error.logId,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
18
65
|
return 2;
|
|
19
66
|
case ErrorCodes.CHECK_TOKEN_FAILED:
|
|
20
67
|
case ErrorCodes.TOKEN_NOT_EXIST:
|
|
21
68
|
case ErrorCodes.CHECK_TOKEN_PERM_FAILED:
|
|
22
|
-
|
|
69
|
+
printStructuredError({
|
|
70
|
+
code: 'AUTH_FAILED',
|
|
71
|
+
message: `鉴权失败(${error.errCode}):${error.errMsg}`,
|
|
72
|
+
details: {
|
|
73
|
+
err_code: error.errCode,
|
|
74
|
+
log_id: error.logId,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
23
77
|
return 3;
|
|
24
78
|
default:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
79
|
+
printStructuredError({
|
|
80
|
+
code: 'API_ERROR',
|
|
81
|
+
message: `API 错误(${error.errCode}):${error.errMsg}`,
|
|
82
|
+
details: {
|
|
83
|
+
err_code: error.errCode,
|
|
84
|
+
log_id: error.logId,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
29
87
|
return 1;
|
|
30
88
|
}
|
|
31
89
|
}
|
|
32
90
|
export function handleCliError(error) {
|
|
33
91
|
if (error instanceof CommanderError) {
|
|
34
92
|
if (error.code !== 'commander.helpDisplayed') {
|
|
35
|
-
|
|
93
|
+
printStructuredError({
|
|
94
|
+
code: 'COMMAND_ARGUMENT_ERROR',
|
|
95
|
+
message: error.message,
|
|
96
|
+
});
|
|
36
97
|
}
|
|
37
98
|
return error.exitCode;
|
|
38
99
|
}
|
|
39
100
|
if (error instanceof CliError) {
|
|
40
|
-
|
|
101
|
+
printStructuredError({
|
|
102
|
+
code: error.code,
|
|
103
|
+
message: error.message,
|
|
104
|
+
details: error.details,
|
|
105
|
+
});
|
|
41
106
|
return error.exitCode;
|
|
42
107
|
}
|
|
43
108
|
if (error instanceof MeegoError) {
|
|
44
109
|
return handleMeegoError(error);
|
|
45
110
|
}
|
|
46
111
|
if (error instanceof Error) {
|
|
47
|
-
|
|
112
|
+
printStructuredError({
|
|
113
|
+
code: 'UNEXPECTED_ERROR',
|
|
114
|
+
message: error.message,
|
|
115
|
+
});
|
|
48
116
|
return 1;
|
|
49
117
|
}
|
|
50
|
-
|
|
118
|
+
printStructuredError({
|
|
119
|
+
code: 'UNKNOWN_ERROR',
|
|
120
|
+
message: '未知错误',
|
|
121
|
+
});
|
|
51
122
|
return 1;
|
|
52
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meegle-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Plugin-only CLI for Feishu Project (Meegle) based on meeglesdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"README.md",
|
|
12
12
|
"README-en.md",
|
|
13
13
|
"RELEASE.md",
|
|
14
|
-
"examples"
|
|
14
|
+
"examples",
|
|
15
|
+
"skills"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"clean": "rm -rf dist",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: meegle-cli-usage
|
|
3
|
+
description: Use the meegle CLI shipped in this package instead of raw SDK or guessed API calls when an agent needs to query spaces, work items, views, PMO baselines, or related-field searches such as planning_version. Use for CLI-based Meegle automation, debugging, and analysis. Includes safety rules for write operations (`--yes` in non-interactive environments), view-type resolution, and relation-ID based filtering.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Meegle CLI Usage
|
|
7
|
+
|
|
8
|
+
Use `meegle` as the default entrypoint for Meegle operations in this package. Prefer invoking the CLI over reconstructing request payloads from memory.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
1. Resolve the command path.
|
|
13
|
+
- Try `meegle --help` first.
|
|
14
|
+
- If `meegle` is not in PATH, use `which meegle` or `npm bin -g`.
|
|
15
|
+
- If working inside this package without a global install, use `node_modules/.bin/meegle`.
|
|
16
|
+
2. Resolve the command shape before guessing.
|
|
17
|
+
- Run `meegle --help` or `<subcommand> --help`.
|
|
18
|
+
3. Separate read and write intent.
|
|
19
|
+
- Treat read commands as default-safe.
|
|
20
|
+
- Treat write commands as opt-in only.
|
|
21
|
+
- In non-interactive environments, only add `--yes` when the user explicitly approved the write.
|
|
22
|
+
|
|
23
|
+
## Safety Rules
|
|
24
|
+
|
|
25
|
+
1. Refuse to add `--yes` implicitly.
|
|
26
|
+
2. Prefer read-only discovery before any write.
|
|
27
|
+
3. Echo the target scope in reasoning before running a write:
|
|
28
|
+
- `profile`
|
|
29
|
+
- `projectKey`
|
|
30
|
+
- `workItemType`
|
|
31
|
+
- target `id` or `ids`
|
|
32
|
+
4. If the task can be solved with `get`, `list`, `search`, or `query`, do that first.
|
|
33
|
+
|
|
34
|
+
## Choose the Right Command
|
|
35
|
+
|
|
36
|
+
### Query a view
|
|
37
|
+
|
|
38
|
+
Do not infer the view type from a page URL.
|
|
39
|
+
|
|
40
|
+
Always:
|
|
41
|
+
|
|
42
|
+
1. Run `view list` with the candidate `view_id`
|
|
43
|
+
2. Inspect `view_type`
|
|
44
|
+
3. Use:
|
|
45
|
+
- `view fix-items` for fixed view (`view_type = 2`)
|
|
46
|
+
- `view panoramic-items` only when the view is not fixed
|
|
47
|
+
|
|
48
|
+
### Filter by a related field
|
|
49
|
+
|
|
50
|
+
For fields such as `planning_version` or `planning_sprint`:
|
|
51
|
+
|
|
52
|
+
1. Do not use `workitem search filter` with `field_value_pairs`
|
|
53
|
+
2. Resolve the related work item instance ID first
|
|
54
|
+
3. Use `workitem search by-params`
|
|
55
|
+
4. Pass the related work item **ID list**, not the display name
|
|
56
|
+
|
|
57
|
+
This is the most common agent mistake. `field_value_pairs` belongs to create/update flows, not search flows.
|
|
58
|
+
|
|
59
|
+
### Build a PMO baseline
|
|
60
|
+
|
|
61
|
+
For PMO-style analysis:
|
|
62
|
+
|
|
63
|
+
1. Resolve the target view
|
|
64
|
+
2. Fetch the item IDs from the view
|
|
65
|
+
3. Fetch work item details with selected fields
|
|
66
|
+
4. Summarize:
|
|
67
|
+
- state distribution
|
|
68
|
+
- owner distribution
|
|
69
|
+
- priority coverage
|
|
70
|
+
- due-date coverage
|
|
71
|
+
- age / stale updates
|
|
72
|
+
|
|
73
|
+
Do not replace a view-scoped baseline with the whole space backlog unless the user explicitly asks for that.
|
|
74
|
+
|
|
75
|
+
## Write Operations
|
|
76
|
+
|
|
77
|
+
Use write commands only after explicit user approval.
|
|
78
|
+
|
|
79
|
+
Typical write commands:
|
|
80
|
+
|
|
81
|
+
- `workitem create`
|
|
82
|
+
- `workitem update`
|
|
83
|
+
- `comment add/update/remove`
|
|
84
|
+
- `subtask create/update/remove/operate`
|
|
85
|
+
- `workflow state-change`
|
|
86
|
+
- `workflow node-operate`
|
|
87
|
+
|
|
88
|
+
In scripts, CI, or agent runners:
|
|
89
|
+
|
|
90
|
+
- pass `--yes` only when approved
|
|
91
|
+
- expect non-interactive write rejection if `--yes` is missing
|
|
92
|
+
|
|
93
|
+
## Use the Bundled Examples
|
|
94
|
+
|
|
95
|
+
Read [references/command-recipes.md](./references/command-recipes.md) when you need:
|
|
96
|
+
|
|
97
|
+
- view lookup examples
|
|
98
|
+
- fixed view item queries
|
|
99
|
+
- version lookup by name
|
|
100
|
+
- story lookup by `planning_version`
|
|
101
|
+
- PMO baseline query order
|
|
102
|
+
|
|
103
|
+
Use the package examples directly when possible:
|
|
104
|
+
|
|
105
|
+
- `../../examples/view-list.json`
|
|
106
|
+
- `../../examples/view-fix-items.json`
|
|
107
|
+
- `../../examples/version-search-filter.json`
|
|
108
|
+
- `../../examples/story-by-planning-version.json`
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Command Recipes
|
|
2
|
+
|
|
3
|
+
## Resolve a View
|
|
4
|
+
|
|
5
|
+
Use this first whenever the user gives a page URL or a `view_id`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
meegle --profile default view list \
|
|
9
|
+
--project-key 69zyer \
|
|
10
|
+
--body-file ./examples/view-list.json \
|
|
11
|
+
--json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Interpretation:
|
|
15
|
+
|
|
16
|
+
- `view_type = 2` -> fixed view -> use `view fix-items`
|
|
17
|
+
- otherwise -> inspect whether `view panoramic-items` is the intended path
|
|
18
|
+
|
|
19
|
+
## Fetch Fixed View Items
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
meegle --profile default view fix-items \
|
|
23
|
+
--project-key 69zyer \
|
|
24
|
+
--view-id sypbi-z60 \
|
|
25
|
+
--query-file ./examples/view-fix-items.json \
|
|
26
|
+
--json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use the returned `work_item_id_list` as input to `workitem get`.
|
|
30
|
+
|
|
31
|
+
## Find a Version Work Item by Name
|
|
32
|
+
|
|
33
|
+
Use this when the user speaks in business names such as “260312 version” or “gray release”.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
meegle --profile default workitem search filter \
|
|
37
|
+
--project-key 69zyer \
|
|
38
|
+
--body-file ./examples/version-search-filter.json \
|
|
39
|
+
--json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If multiple version items match:
|
|
43
|
+
|
|
44
|
+
1. surface the candidates
|
|
45
|
+
2. ask the user to confirm the intended version or pick the exact `id`
|
|
46
|
+
|
|
47
|
+
## Find Stories by `planning_version`
|
|
48
|
+
|
|
49
|
+
Never pass the version display name directly into `planning_version`.
|
|
50
|
+
|
|
51
|
+
Correct flow:
|
|
52
|
+
|
|
53
|
+
1. resolve the target version work item `id`
|
|
54
|
+
2. place that numeric `id` into `examples/story-by-planning-version.json`
|
|
55
|
+
3. run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
meegle --profile default workitem search by-params \
|
|
59
|
+
--project-key 69zyer \
|
|
60
|
+
--type story \
|
|
61
|
+
--body-file ./examples/story-by-planning-version.json \
|
|
62
|
+
--json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## PMO Baseline Workflow
|
|
66
|
+
|
|
67
|
+
Use this order:
|
|
68
|
+
|
|
69
|
+
1. `view list`
|
|
70
|
+
2. `view fix-items` / `view panoramic-items`
|
|
71
|
+
3. `workitem get --fields ...`
|
|
72
|
+
4. summarize the returned dataset
|
|
73
|
+
|
|
74
|
+
Recommended PMO fields:
|
|
75
|
+
|
|
76
|
+
- `name`
|
|
77
|
+
- `owner`
|
|
78
|
+
- `priority`
|
|
79
|
+
- `description`
|
|
80
|
+
- `work_item_status`
|
|
81
|
+
- `start_time`
|
|
82
|
+
- `exp_time`
|
|
83
|
+
|
|
84
|
+
Recommended output dimensions:
|
|
85
|
+
|
|
86
|
+
- total count
|
|
87
|
+
- state distribution
|
|
88
|
+
- current node distribution
|
|
89
|
+
- owner concentration
|
|
90
|
+
- missing priority
|
|
91
|
+
- missing due date
|
|
92
|
+
- creation/update time range
|
|
93
|
+
|
|
94
|
+
## Write Command Reminder
|
|
95
|
+
|
|
96
|
+
In non-interactive environments:
|
|
97
|
+
|
|
98
|
+
- add `--yes` only when the user explicitly approved a mutation
|
|
99
|
+
- do not silently convert a read request into a write request
|