@metaphorli/pingcode-cli 0.3.2 → 0.3.3
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 +72 -9
- package/package.json +2 -2
- package/scripts/commands/auth.js +221 -24
- package/scripts/commands/comment.js +556 -0
- package/scripts/commands/context.js +15 -111
- package/scripts/commands/idea.js +1121 -0
- package/scripts/commands/product.js +236 -0
- package/scripts/commands/shared.js +107 -2
- package/scripts/commands/workitem.js +24 -179
- package/scripts/core.js +17 -0
- package/scripts/pingcode.js +3 -0
- package/skills/pingcode/SKILL.md +23 -8
- package/skills/pingcode/references/auth.md +55 -1
- package/skills/pingcode/references/comment.md +52 -0
- package/skills/pingcode/references/ctx.md +3 -3
- package/skills/pingcode/references/idea.md +70 -0
- package/skills/pingcode/references/product.md +48 -0
- package/skills/pingcode/references/workitem.md +2 -4
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const core = require('../core');
|
|
4
|
+
const shared = require('./shared');
|
|
5
|
+
|
|
6
|
+
// ── Help ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function printHelp() {
|
|
9
|
+
console.log([
|
|
10
|
+
'PingCode product — Manage products',
|
|
11
|
+
'',
|
|
12
|
+
'Usage: pingcode product <subcommand> [options]',
|
|
13
|
+
'',
|
|
14
|
+
'Subcommands:',
|
|
15
|
+
' list [options] List products',
|
|
16
|
+
' --keywords TEXT Filter by keywords (matches name and identifier)',
|
|
17
|
+
' --limit N Max results per page',
|
|
18
|
+
'',
|
|
19
|
+
' get <id|name> Show a single product by raw id or name',
|
|
20
|
+
'',
|
|
21
|
+
'Global options:',
|
|
22
|
+
' --base-url URL PingCode base URL',
|
|
23
|
+
' --client-id ID OAuth client ID',
|
|
24
|
+
' --client-secret SECRET OAuth client secret',
|
|
25
|
+
' --token TOKEN Bearer token (skip OAuth)',
|
|
26
|
+
' --user-id ID Current user ID',
|
|
27
|
+
' --user-name NAME Current user name',
|
|
28
|
+
' --workspace-cache PATH Workspace cache file path',
|
|
29
|
+
' --no-workspace-cache Disable workspace cache',
|
|
30
|
+
' --no-token-cache Disable token cache',
|
|
31
|
+
' --dry-run Show API request without executing',
|
|
32
|
+
' --compact Compact output',
|
|
33
|
+
' --grant-type TYPE OAuth grant type: client_credentials, authorization_code, or auto (default; uses cached token type)',
|
|
34
|
+
' --help Show this help',
|
|
35
|
+
].join('\n'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printSubcommandHelp(subcommand) {
|
|
39
|
+
switch (subcommand) {
|
|
40
|
+
case 'list':
|
|
41
|
+
console.log([
|
|
42
|
+
'Usage: pingcode product list [options]',
|
|
43
|
+
'',
|
|
44
|
+
'List products.',
|
|
45
|
+
'',
|
|
46
|
+
'Options:',
|
|
47
|
+
' --keywords TEXT Filter by keywords (matches name and identifier)',
|
|
48
|
+
' --limit N Max results per page',
|
|
49
|
+
].join('\n'));
|
|
50
|
+
break;
|
|
51
|
+
case 'get':
|
|
52
|
+
console.log([
|
|
53
|
+
'Usage: pingcode product get <id|name>',
|
|
54
|
+
'',
|
|
55
|
+
'Show a single product by raw id or name.',
|
|
56
|
+
'',
|
|
57
|
+
'If given a raw id (24-32 hex characters), fetches the product directly.',
|
|
58
|
+
'Otherwise searches products by keywords and returns the first match.',
|
|
59
|
+
].join('\n'));
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
printHelp();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── List subcommand ───────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function parseListArgs(tokens) {
|
|
69
|
+
const args = {
|
|
70
|
+
keywords: null,
|
|
71
|
+
limit: null,
|
|
72
|
+
};
|
|
73
|
+
const stringFlags = {
|
|
74
|
+
'--keywords': 'keywords',
|
|
75
|
+
'--limit': 'limit',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
79
|
+
const arg = tokens[i];
|
|
80
|
+
if (arg in stringFlags) {
|
|
81
|
+
if (i + 1 >= tokens.length) {
|
|
82
|
+
throw new core.PingCodeError(`Flag ${arg} requires a value`);
|
|
83
|
+
}
|
|
84
|
+
args[stringFlags[arg]] = tokens[i + 1];
|
|
85
|
+
i += 1;
|
|
86
|
+
} else if (arg.startsWith('--')) {
|
|
87
|
+
const eqIndex = arg.indexOf('=');
|
|
88
|
+
if (eqIndex !== -1) {
|
|
89
|
+
const flag = arg.slice(0, eqIndex);
|
|
90
|
+
const value = arg.slice(eqIndex + 1);
|
|
91
|
+
if (flag in stringFlags) {
|
|
92
|
+
args[stringFlags[flag]] = value;
|
|
93
|
+
} else {
|
|
94
|
+
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
throw new core.PingCodeError(`Unknown option: ${arg}. Use product list --help for usage.`);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use product list --help for usage.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return args;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runList(client, opts, args) {
|
|
107
|
+
const params = {};
|
|
108
|
+
if (args.keywords) {
|
|
109
|
+
params.keywords = args.keywords;
|
|
110
|
+
}
|
|
111
|
+
if (args.limit) {
|
|
112
|
+
params.page_size = String(args.limit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return await client.request(
|
|
116
|
+
'GET',
|
|
117
|
+
'/v1/ship/products',
|
|
118
|
+
params,
|
|
119
|
+
null,
|
|
120
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Get subcommand ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function parseGetArgs(tokens) {
|
|
127
|
+
let productId = null;
|
|
128
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
129
|
+
const arg = tokens[i];
|
|
130
|
+
if (!arg.startsWith('--')) {
|
|
131
|
+
if (productId === null) {
|
|
132
|
+
productId = arg;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use product get --help for usage.`);
|
|
136
|
+
}
|
|
137
|
+
throw new core.PingCodeError(`Unknown option: ${arg}. Use product get --help for usage.`);
|
|
138
|
+
}
|
|
139
|
+
if (!productId) {
|
|
140
|
+
throw new core.PingCodeError('A product id or name is required. Use product get --help for usage.');
|
|
141
|
+
}
|
|
142
|
+
return { product_id: productId };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const PRODUCT_RAW_ID_RE = /^[0-9a-fA-F]{24,32}$/;
|
|
146
|
+
|
|
147
|
+
async function runGet(client, opts, args) {
|
|
148
|
+
const input = args.product_id;
|
|
149
|
+
let productId;
|
|
150
|
+
|
|
151
|
+
if (PRODUCT_RAW_ID_RE.test(input)) {
|
|
152
|
+
productId = input;
|
|
153
|
+
} else {
|
|
154
|
+
// Search by keywords, resolve to id
|
|
155
|
+
const listResp = await client.request(
|
|
156
|
+
'GET',
|
|
157
|
+
'/v1/ship/products',
|
|
158
|
+
{ keywords: input },
|
|
159
|
+
null,
|
|
160
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
161
|
+
);
|
|
162
|
+
const values = core.pageValues(listResp);
|
|
163
|
+
if (values.length === 0) {
|
|
164
|
+
throw new core.PingCodeError(`No product found matching "${input}"`);
|
|
165
|
+
}
|
|
166
|
+
productId = values[0].id;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return await client.request(
|
|
170
|
+
'GET',
|
|
171
|
+
`/v1/ship/products/${productId}`,
|
|
172
|
+
null,
|
|
173
|
+
null,
|
|
174
|
+
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Main dispatcher ───────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
async function run(argv) {
|
|
181
|
+
const tokens = argv || [];
|
|
182
|
+
|
|
183
|
+
if (tokens.length === 0 || tokens[0] === '--help' || tokens[0] === '-h') {
|
|
184
|
+
printHelp();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const subcommand = tokens[0];
|
|
189
|
+
const remaining = tokens.slice(1);
|
|
190
|
+
|
|
191
|
+
if (remaining.includes('--help') || remaining.includes('-h')) {
|
|
192
|
+
printSubcommandHelp(subcommand);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { opts, remaining: subArgs } = shared.parseGlobalOptions(remaining);
|
|
197
|
+
const client = shared.clientFromOpts(opts);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
let result;
|
|
201
|
+
switch (subcommand) {
|
|
202
|
+
case 'list': {
|
|
203
|
+
const listArgs = parseListArgs(subArgs);
|
|
204
|
+
result = await runList(client, opts, listArgs);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 'get': {
|
|
208
|
+
const getArgs = parseGetArgs(subArgs);
|
|
209
|
+
result = await runGet(client, opts, getArgs);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
default:
|
|
213
|
+
throw new core.PingCodeError(`Unknown product subcommand: ${subcommand}. Use product --help for usage.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (opts.dry_run) {
|
|
217
|
+
core.printJson(result);
|
|
218
|
+
} else if (result !== null && result !== undefined) {
|
|
219
|
+
if (opts.compact) {
|
|
220
|
+
core.printJson(core.compactResponse(result));
|
|
221
|
+
} else {
|
|
222
|
+
core.printJson(result);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (exc) {
|
|
226
|
+
throw exc;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
shared.registerModule('product', {
|
|
231
|
+
name: 'product',
|
|
232
|
+
description: 'Manage PingCode products',
|
|
233
|
+
run,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
module.exports = { run, printHelp };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
// Module registry shared by the dispatcher and command modules.
|
|
4
4
|
// Command modules register themselves here; the dispatcher discovers
|
|
5
5
|
// them without importing command files directly (avoids circular deps).
|
|
6
6
|
|
|
7
|
+
const core = require('../core');
|
|
8
|
+
|
|
7
9
|
const registry = new Map();
|
|
8
10
|
|
|
9
11
|
function registerModule(name, config) {
|
|
@@ -38,4 +40,107 @@ function printModulesHelp() {
|
|
|
38
40
|
console.log(lines.join('\n'));
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
const BASE_GLOBAL_BOOLEAN_FLAGS = new Set([
|
|
44
|
+
'--dry-run', '--compact', '--no-token-cache', '--no-workspace-cache',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const BASE_GLOBAL_STRING_FLAGS = {
|
|
48
|
+
'--base-url': 'base_url',
|
|
49
|
+
'--client-id': 'client_id',
|
|
50
|
+
'--client-secret': 'client_secret',
|
|
51
|
+
'--token': 'token',
|
|
52
|
+
'--user-id': 'user_id',
|
|
53
|
+
'--user-name': 'user_name',
|
|
54
|
+
'--workspace-cache': 'workspace_cache',
|
|
55
|
+
'--grant-type': 'grant_type',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function defaultGlobalOpts(extraBooleanFlags = []) {
|
|
59
|
+
const opts = {
|
|
60
|
+
base_url: process.env.PINGCODE_BASE_URL || core.DEFAULT_BASE_URL,
|
|
61
|
+
client_id: process.env.PINGCODE_CLIENT_ID || null,
|
|
62
|
+
client_secret: process.env.PINGCODE_CLIENT_SECRET || null,
|
|
63
|
+
token: process.env.PINGCODE_ACCESS_TOKEN || null,
|
|
64
|
+
user_id: process.env.PINGCODE_USER_ID || null,
|
|
65
|
+
user_name: process.env.PINGCODE_USER_NAME || null,
|
|
66
|
+
no_token_cache: false,
|
|
67
|
+
workspace_cache: process.env.PINGCODE_WORKSPACE_CACHE || core.DEFAULT_WORKSPACE_CACHE,
|
|
68
|
+
no_workspace_cache: false,
|
|
69
|
+
dry_run: false,
|
|
70
|
+
compact: false,
|
|
71
|
+
grant_type: 'auto',
|
|
72
|
+
};
|
|
73
|
+
for (const flag of extraBooleanFlags) {
|
|
74
|
+
const key = flag.replace(/^--/, '').replace(/-/g, '_');
|
|
75
|
+
opts[key] = false;
|
|
76
|
+
}
|
|
77
|
+
return opts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseGlobalOptions(tokens, extraBooleanFlags = []) {
|
|
81
|
+
const booleanFlags = new Set([...BASE_GLOBAL_BOOLEAN_FLAGS, ...extraBooleanFlags]);
|
|
82
|
+
const opts = defaultGlobalOpts(extraBooleanFlags);
|
|
83
|
+
const remaining = [];
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
86
|
+
const arg = tokens[i];
|
|
87
|
+
if (arg === '--help' || arg === '-h') {
|
|
88
|
+
remaining.push(arg);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (booleanFlags.has(arg)) {
|
|
92
|
+
const key = arg.replace(/^--/, '').replace(/-/g, '_');
|
|
93
|
+
opts[key] = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const eqIndex = arg.indexOf('=');
|
|
97
|
+
let flag, value, consumedNext = false;
|
|
98
|
+
if (eqIndex !== -1) {
|
|
99
|
+
flag = arg.slice(0, eqIndex);
|
|
100
|
+
value = arg.slice(eqIndex + 1);
|
|
101
|
+
} else if (arg.startsWith('--')) {
|
|
102
|
+
flag = arg;
|
|
103
|
+
if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
|
|
104
|
+
value = tokens[i + 1];
|
|
105
|
+
consumedNext = true;
|
|
106
|
+
} else {
|
|
107
|
+
remaining.push(arg);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
remaining.push(arg);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (flag in BASE_GLOBAL_STRING_FLAGS) {
|
|
115
|
+
opts[BASE_GLOBAL_STRING_FLAGS[flag]] = value;
|
|
116
|
+
if (consumedNext) i += 1;
|
|
117
|
+
} else {
|
|
118
|
+
remaining.push(arg);
|
|
119
|
+
if (consumedNext) remaining.push(value);
|
|
120
|
+
if (consumedNext) i += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { opts, remaining };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function clientFromOpts(opts) {
|
|
127
|
+
const tokenCache = opts.no_token_cache
|
|
128
|
+
? null
|
|
129
|
+
: (process.env.PINGCODE_TOKEN_CACHE || core.DEFAULT_TOKEN_CACHE);
|
|
130
|
+
const workspaceCache = opts.no_workspace_cache ? null : opts.workspace_cache;
|
|
131
|
+
return new core.PingCodeClient({
|
|
132
|
+
base_url: opts.base_url,
|
|
133
|
+
client_id: opts.client_id,
|
|
134
|
+
client_secret: opts.client_secret,
|
|
135
|
+
token: opts.token,
|
|
136
|
+
token_cache: tokenCache,
|
|
137
|
+
workspace_cache: workspaceCache,
|
|
138
|
+
grant_type: opts.grant_type,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
registerModule, getModule, listModules, printModulesHelp,
|
|
144
|
+
BASE_GLOBAL_BOOLEAN_FLAGS, BASE_GLOBAL_STRING_FLAGS,
|
|
145
|
+
defaultGlobalOpts, parseGlobalOptions, clientFromOpts,
|
|
146
|
+
};
|
|
@@ -3,102 +3,14 @@
|
|
|
3
3
|
const core = require('../core');
|
|
4
4
|
const shared = require('./shared');
|
|
5
5
|
|
|
6
|
-
// ──
|
|
7
|
-
const
|
|
8
|
-
'--dry-run', '--compact', '--no-token-cache', '--no-workspace-cache',
|
|
9
|
-
'--all-users', '--all-projects', '--all-sprints',
|
|
10
|
-
]);
|
|
6
|
+
// ── Extra global boolean flags ──────────────────────────────────────
|
|
7
|
+
const EXTRA_BOOLEAN_FLAGS = ['--all-users', '--all-projects', '--all-sprints'];
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'--user-id': 'user_id',
|
|
18
|
-
'--user-name': 'user_name',
|
|
19
|
-
'--workspace-cache': 'workspace_cache',
|
|
20
|
-
'--grant-type': 'grant_type',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function defaultGlobalOpts() {
|
|
24
|
-
return {
|
|
25
|
-
base_url: process.env.PINGCODE_BASE_URL || core.DEFAULT_BASE_URL,
|
|
26
|
-
client_id: process.env.PINGCODE_CLIENT_ID || null,
|
|
27
|
-
client_secret: process.env.PINGCODE_CLIENT_SECRET || null,
|
|
28
|
-
token: process.env.PINGCODE_ACCESS_TOKEN || null,
|
|
29
|
-
user_id: process.env.PINGCODE_USER_ID || null,
|
|
30
|
-
user_name: process.env.PINGCODE_USER_NAME || null,
|
|
31
|
-
no_token_cache: false,
|
|
32
|
-
workspace_cache: process.env.PINGCODE_WORKSPACE_CACHE || core.DEFAULT_WORKSPACE_CACHE,
|
|
33
|
-
no_workspace_cache: false,
|
|
34
|
-
dry_run: false,
|
|
35
|
-
compact: false,
|
|
36
|
-
all_users: false,
|
|
37
|
-
all_projects: false,
|
|
38
|
-
all_sprints: false,
|
|
39
|
-
grant_type: 'auto',
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function parseGlobalOptions(tokens) {
|
|
44
|
-
const opts = defaultGlobalOpts();
|
|
45
|
-
const remaining = [];
|
|
46
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
47
|
-
const arg = tokens[i];
|
|
48
|
-
if (arg === '--help' || arg === '-h') {
|
|
49
|
-
remaining.push(arg);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (GLOBAL_BOOLEAN_FLAGS.has(arg)) {
|
|
53
|
-
const key = arg.replace(/^--/, '').replace(/-/g, '_');
|
|
54
|
-
opts[key] = true;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const eqIndex = arg.indexOf('=');
|
|
58
|
-
let flag, value, consumedNext = false;
|
|
59
|
-
if (eqIndex !== -1) {
|
|
60
|
-
flag = arg.slice(0, eqIndex);
|
|
61
|
-
value = arg.slice(eqIndex + 1);
|
|
62
|
-
} else if (arg.startsWith('--')) {
|
|
63
|
-
flag = arg;
|
|
64
|
-
if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
|
|
65
|
-
value = tokens[i + 1];
|
|
66
|
-
consumedNext = true;
|
|
67
|
-
} else {
|
|
68
|
-
// Boolean-like flag with no value — could be a subcommand flag
|
|
69
|
-
remaining.push(arg);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
remaining.push(arg);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (flag in GLOBAL_STRING_FLAGS) {
|
|
77
|
-
opts[GLOBAL_STRING_FLAGS[flag]] = value;
|
|
78
|
-
if (consumedNext) i += 1;
|
|
79
|
-
} else {
|
|
80
|
-
// Not a recognized global flag — pass through as subcommand arg
|
|
81
|
-
remaining.push(arg);
|
|
82
|
-
if (consumedNext) remaining.push(value);
|
|
83
|
-
if (consumedNext) i += 1;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return { opts, remaining };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function clientFromOpts(opts) {
|
|
90
|
-
const tokenCache = opts.no_token_cache ? null : (process.env.PINGCODE_TOKEN_CACHE || core.DEFAULT_TOKEN_CACHE);
|
|
91
|
-
const workspaceCache = opts.no_workspace_cache ? null : opts.workspace_cache;
|
|
92
|
-
return new core.PingCodeClient({
|
|
93
|
-
base_url: opts.base_url,
|
|
94
|
-
client_id: opts.client_id,
|
|
95
|
-
client_secret: opts.client_secret,
|
|
96
|
-
token: opts.token,
|
|
97
|
-
token_cache: tokenCache,
|
|
98
|
-
workspace_cache: workspaceCache,
|
|
99
|
-
grant_type: opts.grant_type,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
9
|
+
// Combined set used by sub-parsers to skip globally consumed flags
|
|
10
|
+
const ALL_BOOLEAN_FLAGS = new Set([
|
|
11
|
+
...shared.BASE_GLOBAL_BOOLEAN_FLAGS,
|
|
12
|
+
...EXTRA_BOOLEAN_FLAGS,
|
|
13
|
+
]);
|
|
102
14
|
|
|
103
15
|
// ── Cache lookup helpers ──────────────────────────────────────────────
|
|
104
16
|
|
|
@@ -234,7 +146,7 @@ function parseListArgs(tokens) {
|
|
|
234
146
|
} else {
|
|
235
147
|
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
236
148
|
}
|
|
237
|
-
} else if (!(arg
|
|
149
|
+
} else if (!ALL_BOOLEAN_FLAGS.has(arg)) {
|
|
238
150
|
throw new core.PingCodeError(`Unknown option: ${arg}. Use workitem list --help for usage.`);
|
|
239
151
|
}
|
|
240
152
|
} else {
|
|
@@ -341,7 +253,7 @@ function parseCreateArgs(tokens) {
|
|
|
341
253
|
} else {
|
|
342
254
|
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
343
255
|
}
|
|
344
|
-
} else if (!(arg
|
|
256
|
+
} else if (!ALL_BOOLEAN_FLAGS.has(arg)) {
|
|
345
257
|
throw new core.PingCodeError(`Unknown option: ${arg}. Use workitem create --help for usage.`);
|
|
346
258
|
}
|
|
347
259
|
} else {
|
|
@@ -441,61 +353,6 @@ async function runCreate(client, opts, args) {
|
|
|
441
353
|
);
|
|
442
354
|
}
|
|
443
355
|
|
|
444
|
-
// ── Show subcommand ──────────────────────────────────────────────────
|
|
445
|
-
|
|
446
|
-
function parseShowArgs(tokens) {
|
|
447
|
-
// First non-flag token is the id/identifier
|
|
448
|
-
let target = null;
|
|
449
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
450
|
-
const arg = tokens[i];
|
|
451
|
-
if (!arg.startsWith('--')) {
|
|
452
|
-
if (target === null) {
|
|
453
|
-
target = arg;
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use workitem show --help for usage.`);
|
|
457
|
-
}
|
|
458
|
-
// Consume flag+value for global options (already parsed)
|
|
459
|
-
if (GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
|
|
460
|
-
if (GLOBAL_STRING_FLAGS[arg]) {
|
|
461
|
-
i += 1;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
// For --help (already handled in parseGlobalOptions)
|
|
465
|
-
}
|
|
466
|
-
if (!target) {
|
|
467
|
-
throw new core.PingCodeError('A work item id or identifier is required. Use workitem show --help for usage.');
|
|
468
|
-
}
|
|
469
|
-
return { target };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
async function runShow(client, opts, args) {
|
|
473
|
-
const params = {};
|
|
474
|
-
// Detect if target is an identifier (uppercase letters + dash + number)
|
|
475
|
-
const isIdFormat = /^[A-Z]{3,6}-\d+$/.test(args.target);
|
|
476
|
-
if (isIdFormat) {
|
|
477
|
-
params.identifier = args.target;
|
|
478
|
-
} else {
|
|
479
|
-
params.id = args.target;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const result = await client.request(
|
|
483
|
-
'GET',
|
|
484
|
-
'/v1/project/work_items',
|
|
485
|
-
params,
|
|
486
|
-
null,
|
|
487
|
-
{ dry_run: opts.dry_run, use_workspace_cache: true },
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
if (opts.dry_run) return result;
|
|
491
|
-
|
|
492
|
-
// For real results, compact if requested, otherwise return first match
|
|
493
|
-
if (opts.compact) {
|
|
494
|
-
return core.compactResponse(result);
|
|
495
|
-
}
|
|
496
|
-
return result;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
356
|
// ── Get subcommand ───────────────────────────────────────────────────
|
|
500
357
|
|
|
501
358
|
function parseGetArgs(tokens) {
|
|
@@ -509,8 +366,8 @@ function parseGetArgs(tokens) {
|
|
|
509
366
|
}
|
|
510
367
|
throw new core.PingCodeError(`Unexpected argument: ${arg}. Use workitem get --help for usage.`);
|
|
511
368
|
}
|
|
512
|
-
if (
|
|
513
|
-
if (
|
|
369
|
+
if (ALL_BOOLEAN_FLAGS.has(arg)) continue;
|
|
370
|
+
if (shared.BASE_GLOBAL_STRING_FLAGS[arg]) {
|
|
514
371
|
i += 1;
|
|
515
372
|
continue;
|
|
516
373
|
}
|
|
@@ -521,6 +378,12 @@ function parseGetArgs(tokens) {
|
|
|
521
378
|
return { work_item_id: workItemId };
|
|
522
379
|
}
|
|
523
380
|
|
|
381
|
+
function isIdentifier(arg) {
|
|
382
|
+
// PingCode identifiers look like: PROJECT_KEY-NUMBER (e.g., SCR-1, TASK-42)
|
|
383
|
+
// Project keys are typically 3-6 uppercase letters.
|
|
384
|
+
return /^[A-Z]{3,6}-\d+$/.test(arg);
|
|
385
|
+
}
|
|
386
|
+
|
|
524
387
|
async function runGet(client, opts, args) {
|
|
525
388
|
if (isIdentifier(args.work_item_id)) {
|
|
526
389
|
const resolutionParams = { identifier: args.work_item_id };
|
|
@@ -642,11 +505,11 @@ function parseUpdateArgs(tokens) {
|
|
|
642
505
|
if (flag in stringFlags) {
|
|
643
506
|
args[stringFlags[flag]] = value;
|
|
644
507
|
} else {
|
|
645
|
-
if (!(flag
|
|
508
|
+
if (!ALL_BOOLEAN_FLAGS.has(flag) && !(flag in shared.BASE_GLOBAL_STRING_FLAGS)) {
|
|
646
509
|
throw new core.PingCodeError(`Unknown option: ${flag}`);
|
|
647
510
|
}
|
|
648
511
|
}
|
|
649
|
-
} else if (!(arg
|
|
512
|
+
} else if (!ALL_BOOLEAN_FLAGS.has(arg)) {
|
|
650
513
|
throw new core.PingCodeError(`Unknown option: ${arg}. Use workitem update --help for usage.`);
|
|
651
514
|
}
|
|
652
515
|
}
|
|
@@ -666,12 +529,6 @@ function parseUpdateArgs(tokens) {
|
|
|
666
529
|
return args;
|
|
667
530
|
}
|
|
668
531
|
|
|
669
|
-
function isIdentifier(arg) {
|
|
670
|
-
// PingCode identifiers look like: PROJECT_KEY-NUMBER (e.g., SCR-1, TASK-42)
|
|
671
|
-
// Project keys are typically 3-6 uppercase letters.
|
|
672
|
-
return /^[A-Z]{3,6}-\d+$/.test(arg);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
532
|
function parseNumber(value, label) {
|
|
676
533
|
const parsed = Number(value);
|
|
677
534
|
if (Number.isNaN(parsed)) {
|
|
@@ -831,8 +688,6 @@ function printHelp() {
|
|
|
831
688
|
' --description TEXT Description text',
|
|
832
689
|
' --parent <id|identifier> Parent work item',
|
|
833
690
|
'',
|
|
834
|
-
' show <id|identifier> Show a single work item',
|
|
835
|
-
'',
|
|
836
691
|
' get <id|identifier> Get a single work item by id or identifier',
|
|
837
692
|
'',
|
|
838
693
|
' update <id|identifier> Update a work item',
|
|
@@ -913,18 +768,13 @@ function printSubcommandHelp(subcommand) {
|
|
|
913
768
|
' --parent <id|identifier> Parent work item',
|
|
914
769
|
].join('\n'));
|
|
915
770
|
break;
|
|
916
|
-
case 'show':
|
|
917
|
-
console.log([
|
|
918
|
-
'Usage: pingcode workitem show <id|identifier>',
|
|
919
|
-
'',
|
|
920
|
-
'Show a single work item by id or identifier.',
|
|
921
|
-
].join('\n'));
|
|
922
|
-
break;
|
|
923
771
|
case 'get':
|
|
924
772
|
console.log([
|
|
925
773
|
'Usage: pingcode workitem get <id|identifier>',
|
|
926
774
|
'',
|
|
927
775
|
'Get a single work item by id or identifier.',
|
|
776
|
+
'',
|
|
777
|
+
'Identifiers (e.g. SCR-123) are resolved to an id, then the single item is fetched.',
|
|
928
778
|
].join('\n'));
|
|
929
779
|
break;
|
|
930
780
|
case 'update':
|
|
@@ -979,10 +829,10 @@ async function run(argv) {
|
|
|
979
829
|
return;
|
|
980
830
|
}
|
|
981
831
|
|
|
982
|
-
// Parse global options from remaining tokens
|
|
983
|
-
const { opts, remaining: subArgs } = parseGlobalOptions(remaining);
|
|
832
|
+
// Parse global options from remaining tokens, passing workitem-specific boolean flags
|
|
833
|
+
const { opts, remaining: subArgs } = shared.parseGlobalOptions(remaining, EXTRA_BOOLEAN_FLAGS);
|
|
984
834
|
|
|
985
|
-
const client = clientFromOpts(opts);
|
|
835
|
+
const client = shared.clientFromOpts(opts);
|
|
986
836
|
|
|
987
837
|
try {
|
|
988
838
|
let result;
|
|
@@ -997,11 +847,6 @@ async function run(argv) {
|
|
|
997
847
|
result = await runCreate(client, opts, args);
|
|
998
848
|
break;
|
|
999
849
|
}
|
|
1000
|
-
case 'show': {
|
|
1001
|
-
const args = parseShowArgs(subArgs);
|
|
1002
|
-
result = await runShow(client, opts, args);
|
|
1003
|
-
break;
|
|
1004
|
-
}
|
|
1005
850
|
case 'get': {
|
|
1006
851
|
const args = parseGetArgs(subArgs);
|
|
1007
852
|
result = await runGet(client, opts, args);
|
package/scripts/core.js
CHANGED
|
@@ -1185,6 +1185,22 @@ function pathIsListWorkItems(rawPath, baseUrl = DEFAULT_BASE_URL) {
|
|
|
1185
1185
|
return normalizePath(rawPath, baseUrl) === '/v1/project/work_items';
|
|
1186
1186
|
}
|
|
1187
1187
|
|
|
1188
|
+
async function resolveWorkItemIdentifier(client, identifier) {
|
|
1189
|
+
const params = { identifier };
|
|
1190
|
+
const response = await client.request(
|
|
1191
|
+
'GET',
|
|
1192
|
+
'/v1/project/work_items',
|
|
1193
|
+
params,
|
|
1194
|
+
null,
|
|
1195
|
+
{ dry_run: false, use_workspace_cache: true },
|
|
1196
|
+
);
|
|
1197
|
+
const values = pageValues(response);
|
|
1198
|
+
if (values.length === 0) {
|
|
1199
|
+
throw new PingCodeError(`No work item found with identifier ${identifier}`);
|
|
1200
|
+
}
|
|
1201
|
+
return values[0].id;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1188
1204
|
function printJson(data) {
|
|
1189
1205
|
const sorted = sortKeys(data);
|
|
1190
1206
|
console.log(JSON.stringify(sorted, null, 2));
|
|
@@ -1368,5 +1384,6 @@ module.exports = {
|
|
|
1368
1384
|
applyDefaultWorkItemFilters,
|
|
1369
1385
|
ensureWorkItemWorkspaceContext,
|
|
1370
1386
|
applyDefaultWorkItemCreateBody,
|
|
1387
|
+
resolveWorkItemIdentifier,
|
|
1371
1388
|
printJson,
|
|
1372
1389
|
};
|
package/scripts/pingcode.js
CHANGED
|
@@ -13,7 +13,10 @@ module.exports = { ...core };
|
|
|
13
13
|
// Import command modules so they self-register with the shared registry.
|
|
14
14
|
// (Side-effect imports; no circular dependency because these import from
|
|
15
15
|
// ./shared and ../core, never from ../pingcode.)
|
|
16
|
+
require('./commands/comment');
|
|
16
17
|
require('./commands/context');
|
|
18
|
+
require('./commands/idea');
|
|
19
|
+
require('./commands/product');
|
|
17
20
|
require('./commands/workitem');
|
|
18
21
|
require('./commands/auth');
|
|
19
22
|
|