@niiiiiiile/iw-jira-cli 0.5.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/.env.example +14 -0
- package/README.md +277 -0
- package/dist/adf-mentions.d.ts +3 -0
- package/dist/adf-mentions.js +103 -0
- package/dist/adf.d.ts +4 -0
- package/dist/adf.js +38 -0
- package/dist/agent-compact.d.ts +164 -0
- package/dist/agent-compact.js +132 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +4 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +101 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +47 -0
- package/dist/issue-cli.d.ts +127 -0
- package/dist/issue-cli.js +407 -0
- package/dist/issue-fields.d.ts +4 -0
- package/dist/issue-fields.js +20 -0
- package/dist/jira-client.d.ts +67 -0
- package/dist/jira-client.js +77 -0
- package/dist/jira-env.d.ts +10 -0
- package/dist/jira-env.js +23 -0
- package/dist/jsonl-lines.d.ts +3 -0
- package/dist/jsonl-lines.js +57 -0
- package/dist/load-dotenv.d.ts +1 -0
- package/dist/load-dotenv.js +24 -0
- package/dist/output.d.ts +7 -0
- package/dist/output.js +9 -0
- package/dist/parse-jira-input.d.ts +9 -0
- package/dist/parse-jira-input.js +97 -0
- package/dist/profile-cli.d.ts +2 -0
- package/dist/profile-cli.js +105 -0
- package/dist/project-cli.d.ts +14 -0
- package/dist/project-cli.js +36 -0
- package/dist/shared.d.ts +8 -0
- package/dist/shared.js +8 -0
- package/dist/user-cli.d.ts +15 -0
- package/dist/user-cli.js +37 -0
- package/package.json +52 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { Cli, z } from 'incur';
|
|
2
|
+
import { plainTextFromAdf } from './adf.js';
|
|
3
|
+
import { descriptionToAdf } from './adf-mentions.js';
|
|
4
|
+
import { resolveCredentials } from './config.js';
|
|
5
|
+
import { ISSUE_DETAIL_FIELDS, ISSUE_LIST_FIELDS } from './issue-fields.js';
|
|
6
|
+
import { jiraRequest, toIssueSummary } from './jira-client.js';
|
|
7
|
+
import { slimComments, slimCreate, slimIssueEnvelope, slimSearch, slimTransitions, slimUpdate, } from './agent-compact.js';
|
|
8
|
+
import { finalizeCompactOutput } from './output.js';
|
|
9
|
+
import { parseIssueKey, parseProjectRef } from './parse-jira-input.js';
|
|
10
|
+
import { authOptions } from './shared.js';
|
|
11
|
+
const DEFAULT_JQL = 'assignee = currentUser() AND resolution = Unresolved ORDER BY updated DESC';
|
|
12
|
+
/**
|
|
13
|
+
* accountId または "email:user@example.com" 形式から accountId を解決する。
|
|
14
|
+
* "none" は null(割当解除)を意味するため呼び出し元で事前に除外すること。
|
|
15
|
+
*/
|
|
16
|
+
async function resolveAssigneeAccountId(jiraEnv, assignee) {
|
|
17
|
+
const emailPrefix = /^email:/i;
|
|
18
|
+
if (emailPrefix.test(assignee)) {
|
|
19
|
+
const email = assignee.replace(emailPrefix, '').trim();
|
|
20
|
+
if (!email)
|
|
21
|
+
throw new Error('--assignee email:... にメールアドレスがありません');
|
|
22
|
+
const users = await jiraRequest(jiraEnv, `/user/search?query=${encodeURIComponent(email)}&maxResults=25`);
|
|
23
|
+
const exact = users.find((u) => u.emailAddress?.toLowerCase() === email.toLowerCase());
|
|
24
|
+
const u = exact ?? users[0];
|
|
25
|
+
if (!u)
|
|
26
|
+
throw new Error(`ユーザーが見つかりません: ${email}`);
|
|
27
|
+
return u.accountId;
|
|
28
|
+
}
|
|
29
|
+
return assignee;
|
|
30
|
+
}
|
|
31
|
+
export async function loadIssueSummary(env, ref) {
|
|
32
|
+
const key = parseIssueKey(ref);
|
|
33
|
+
const fields = [...ISSUE_DETAIL_FIELDS].join(',');
|
|
34
|
+
const raw = await jiraRequest(env, `/issue/${encodeURIComponent(key)}?fields=${encodeURIComponent(fields)}`);
|
|
35
|
+
return { issue: toIssueSummary(raw) };
|
|
36
|
+
}
|
|
37
|
+
function resolveSearchJql(opts) {
|
|
38
|
+
if (opts.jql !== undefined && opts.jql.length > 0)
|
|
39
|
+
return opts.jql;
|
|
40
|
+
const pRaw = opts.project?.trim() || opts.target?.trim();
|
|
41
|
+
if (pRaw) {
|
|
42
|
+
const p = parseProjectRef(pRaw);
|
|
43
|
+
return `project = ${p} ORDER BY updated DESC`;
|
|
44
|
+
}
|
|
45
|
+
return DEFAULT_JQL;
|
|
46
|
+
}
|
|
47
|
+
export const issueCli = Cli.create('issue', {
|
|
48
|
+
description: '課題の取得・検索・作成・コメント。説明・コメントで @[accountId] / @[email:...] メンション可。user search で accountId 確認',
|
|
49
|
+
})
|
|
50
|
+
.command('get', {
|
|
51
|
+
description: '課題を1件取得(キー WEC-41 または browse URL など)',
|
|
52
|
+
args: z.object({
|
|
53
|
+
ref: z
|
|
54
|
+
.string()
|
|
55
|
+
.describe('課題キー(WEC-41)または URL(.../browse/WEC-41、selectedIssue= 付きボード URL 等)'),
|
|
56
|
+
}),
|
|
57
|
+
options: authOptions,
|
|
58
|
+
output: z.any(),
|
|
59
|
+
async run(c) {
|
|
60
|
+
const full = await loadIssueSummary(resolveCredentials(c.options), c.args.ref);
|
|
61
|
+
return finalizeCompactOutput(c, full.issue, (issue) => slimIssueEnvelope(issue, true));
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
.command('search', {
|
|
65
|
+
description: 'JQL enhanced search。第1引数にプロジェクトキーまたはボード URL も可',
|
|
66
|
+
args: z.object({
|
|
67
|
+
target: z
|
|
68
|
+
.string()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('プロジェクトキー(WEC)または .../projects/WEC/... の URL(--project 未指定時)'),
|
|
71
|
+
}),
|
|
72
|
+
options: z.object({
|
|
73
|
+
jql: z.string().optional().describe('JQL(最優先)'),
|
|
74
|
+
project: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('プロジェクトキーまたは URL(第1引数より優先)'),
|
|
78
|
+
limit: z.coerce.number().int().min(1).max(100).default(20).describe('最大件数'),
|
|
79
|
+
nextPageToken: z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('前回レスポンスの nextPageToken(ページング)'),
|
|
83
|
+
...authOptions.shape,
|
|
84
|
+
}),
|
|
85
|
+
output: z.any(),
|
|
86
|
+
async run(c) {
|
|
87
|
+
const creds = resolveCredentials(c.options);
|
|
88
|
+
const jql = resolveSearchJql({
|
|
89
|
+
jql: c.options.jql,
|
|
90
|
+
project: c.options.project,
|
|
91
|
+
target: c.args.target,
|
|
92
|
+
});
|
|
93
|
+
const body = {
|
|
94
|
+
jql,
|
|
95
|
+
maxResults: c.options.limit,
|
|
96
|
+
fields: [...ISSUE_LIST_FIELDS],
|
|
97
|
+
};
|
|
98
|
+
if (c.options.nextPageToken) {
|
|
99
|
+
body.nextPageToken = c.options.nextPageToken;
|
|
100
|
+
}
|
|
101
|
+
const res = await jiraRequest(creds, '/search/jql', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
const full = {
|
|
106
|
+
count: res.issues.length,
|
|
107
|
+
isLast: res.isLast,
|
|
108
|
+
nextPageToken: res.nextPageToken ?? null,
|
|
109
|
+
issues: res.issues.map((i) => toIssueSummary(i)),
|
|
110
|
+
jql,
|
|
111
|
+
};
|
|
112
|
+
return finalizeCompactOutput(c, full, (data) => slimSearch(data, true));
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
.command('create', {
|
|
116
|
+
description: '課題を作成',
|
|
117
|
+
options: z.object({
|
|
118
|
+
project: z
|
|
119
|
+
.string()
|
|
120
|
+
.min(1)
|
|
121
|
+
.describe('プロジェクトキー(WEC)または .../projects/WEC/... の URL'),
|
|
122
|
+
summary: z.string().min(1).describe('要約'),
|
|
123
|
+
type: z.string().default('Task').describe('課題タイプ名(例: Task, Bug, Story)'),
|
|
124
|
+
description: z
|
|
125
|
+
.string()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('説明(改行可)。メンション: @[712020:uuid...] または @[email:user@example.com](user search で確認)'),
|
|
128
|
+
parent: z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe('親 Issue キー(INT1-110)または URL(Epic リンク・サブタスク親)'),
|
|
132
|
+
assignee: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe('担当者 accountId または email:user@example.com'),
|
|
136
|
+
labels: z
|
|
137
|
+
.string()
|
|
138
|
+
.optional()
|
|
139
|
+
.describe('ラベル(カンマ区切り、例: bug,backend)'),
|
|
140
|
+
priority: z
|
|
141
|
+
.string()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe('優先度名(例: Highest, High, Medium, Low, Lowest)'),
|
|
144
|
+
'start-date': z
|
|
145
|
+
.string()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe('開始日(YYYY-MM-DD)'),
|
|
148
|
+
'due-date': z
|
|
149
|
+
.string()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe('期日(YYYY-MM-DD)'),
|
|
152
|
+
...authOptions.shape,
|
|
153
|
+
}),
|
|
154
|
+
output: z.any(),
|
|
155
|
+
async run(c) {
|
|
156
|
+
const creds = resolveCredentials(c.options);
|
|
157
|
+
const projectKey = parseProjectRef(c.options.project);
|
|
158
|
+
const fields = {
|
|
159
|
+
project: { key: projectKey },
|
|
160
|
+
summary: c.options.summary,
|
|
161
|
+
issuetype: { name: c.options.type },
|
|
162
|
+
};
|
|
163
|
+
if (c.options.description !== undefined && c.options.description.length > 0) {
|
|
164
|
+
fields.description = await descriptionToAdf(creds, c.options.description);
|
|
165
|
+
}
|
|
166
|
+
if (c.options.parent !== undefined && c.options.parent.length > 0) {
|
|
167
|
+
fields.parent = { key: parseIssueKey(c.options.parent) };
|
|
168
|
+
}
|
|
169
|
+
if (c.options.assignee !== undefined && c.options.assignee.length > 0) {
|
|
170
|
+
const accountId = await resolveAssigneeAccountId(creds, c.options.assignee);
|
|
171
|
+
fields.assignee = { accountId };
|
|
172
|
+
}
|
|
173
|
+
if (c.options.labels !== undefined && c.options.labels.length > 0) {
|
|
174
|
+
fields.labels = c.options.labels.split(',').map((s) => s.trim()).filter(Boolean);
|
|
175
|
+
}
|
|
176
|
+
if (c.options.priority !== undefined && c.options.priority.length > 0) {
|
|
177
|
+
fields.priority = { name: c.options.priority };
|
|
178
|
+
}
|
|
179
|
+
if (c.options['start-date'] !== undefined && c.options['start-date'].length > 0) {
|
|
180
|
+
fields.customfield_10015 = c.options['start-date'];
|
|
181
|
+
}
|
|
182
|
+
if (c.options['due-date'] !== undefined && c.options['due-date'].length > 0) {
|
|
183
|
+
fields.duedate = c.options['due-date'];
|
|
184
|
+
}
|
|
185
|
+
const created = await jiraRequest(creds, '/issue', {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
body: JSON.stringify({ fields }),
|
|
188
|
+
});
|
|
189
|
+
const full = { key: created.key, id: created.id, self: created.self };
|
|
190
|
+
return finalizeCompactOutput(c, full, (data) => slimCreate(data, true));
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
.command('update', {
|
|
194
|
+
description: 'Issue を更新(summary / description / assignee / parent / labels / priority)',
|
|
195
|
+
args: z.object({
|
|
196
|
+
ref: z.string().describe('課題キー(WEC-41)または URL'),
|
|
197
|
+
}),
|
|
198
|
+
options: z.object({
|
|
199
|
+
summary: z.string().optional().describe('新しい要約'),
|
|
200
|
+
description: z
|
|
201
|
+
.string()
|
|
202
|
+
.optional()
|
|
203
|
+
.describe('新しい説明(改行可・@[...] メンション可。空文字でクリア)'),
|
|
204
|
+
assignee: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe('担当者 accountId または email:user@example.com("none" で未割当)'),
|
|
208
|
+
parent: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe('親 Issue キー(INT1-110)または URL("none" でクリア)'),
|
|
212
|
+
labels: z
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.describe('ラベル(カンマ区切り。空文字でリセット)'),
|
|
216
|
+
priority: z
|
|
217
|
+
.string()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe('優先度名(例: Highest, High, Medium, Low, Lowest)'),
|
|
220
|
+
'start-date': z
|
|
221
|
+
.string()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe('開始日(YYYY-MM-DD。"none" でクリア)'),
|
|
224
|
+
'due-date': z
|
|
225
|
+
.string()
|
|
226
|
+
.optional()
|
|
227
|
+
.describe('期日(YYYY-MM-DD。"none" でクリア)'),
|
|
228
|
+
...authOptions.shape,
|
|
229
|
+
}),
|
|
230
|
+
output: z.any(),
|
|
231
|
+
async run(c) {
|
|
232
|
+
const creds = resolveCredentials(c.options);
|
|
233
|
+
const key = parseIssueKey(c.args.ref);
|
|
234
|
+
const fields = {};
|
|
235
|
+
if (c.options.summary !== undefined) {
|
|
236
|
+
fields.summary = c.options.summary;
|
|
237
|
+
}
|
|
238
|
+
if (c.options.description !== undefined) {
|
|
239
|
+
fields.description =
|
|
240
|
+
c.options.description.length > 0
|
|
241
|
+
? await descriptionToAdf(creds, c.options.description)
|
|
242
|
+
: null;
|
|
243
|
+
}
|
|
244
|
+
if (c.options.assignee !== undefined) {
|
|
245
|
+
if (c.options.assignee === 'none') {
|
|
246
|
+
fields.assignee = null;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const accountId = await resolveAssigneeAccountId(creds, c.options.assignee);
|
|
250
|
+
fields.assignee = { accountId };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (c.options.parent !== undefined) {
|
|
254
|
+
if (c.options.parent === 'none') {
|
|
255
|
+
fields.parent = null;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
fields.parent = { key: parseIssueKey(c.options.parent) };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (c.options.labels !== undefined) {
|
|
262
|
+
fields.labels = c.options.labels
|
|
263
|
+
? c.options.labels.split(',').map((s) => s.trim()).filter(Boolean)
|
|
264
|
+
: [];
|
|
265
|
+
}
|
|
266
|
+
if (c.options.priority !== undefined) {
|
|
267
|
+
fields.priority = c.options.priority === 'none' ? null : { name: c.options.priority };
|
|
268
|
+
}
|
|
269
|
+
if (c.options['start-date'] !== undefined) {
|
|
270
|
+
fields.customfield_10015 =
|
|
271
|
+
c.options['start-date'] === 'none' ? null : c.options['start-date'];
|
|
272
|
+
}
|
|
273
|
+
if (c.options['due-date'] !== undefined) {
|
|
274
|
+
fields.duedate = c.options['due-date'] === 'none' ? null : c.options['due-date'];
|
|
275
|
+
}
|
|
276
|
+
if (Object.keys(fields).length === 0) {
|
|
277
|
+
throw new Error('更新するフィールドを少なくとも1つ指定してください');
|
|
278
|
+
}
|
|
279
|
+
await jiraRequest(creds, `/issue/${encodeURIComponent(key)}`, {
|
|
280
|
+
method: 'PUT',
|
|
281
|
+
body: JSON.stringify({ fields }),
|
|
282
|
+
});
|
|
283
|
+
const full = { ok: true, key };
|
|
284
|
+
return finalizeCompactOutput(c, full, (data) => slimUpdate(data, true));
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
.command('transitions', {
|
|
288
|
+
description: 'Issue の利用可能なステータス遷移一覧を取得',
|
|
289
|
+
args: z.object({
|
|
290
|
+
ref: z.string().describe('課題キー(WEC-41)または URL'),
|
|
291
|
+
}),
|
|
292
|
+
options: authOptions,
|
|
293
|
+
output: z.any(),
|
|
294
|
+
async run(c) {
|
|
295
|
+
const key = parseIssueKey(c.args.ref);
|
|
296
|
+
const creds = resolveCredentials(c.options);
|
|
297
|
+
const res = await jiraRequest(creds, `/issue/${encodeURIComponent(key)}/transitions`);
|
|
298
|
+
const full = {
|
|
299
|
+
key,
|
|
300
|
+
transitions: res.transitions.map((t) => ({
|
|
301
|
+
id: t.id,
|
|
302
|
+
name: t.name,
|
|
303
|
+
toStatus: t.to?.name ?? '',
|
|
304
|
+
})),
|
|
305
|
+
};
|
|
306
|
+
return finalizeCompactOutput(c, full, (data) => slimTransitions(data, true));
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
.command('transition', {
|
|
310
|
+
description: 'Issue のステータスを遷移させる(transitions で id/名前を確認)',
|
|
311
|
+
args: z.object({
|
|
312
|
+
ref: z.string().describe('課題キー(WEC-41)または URL'),
|
|
313
|
+
}),
|
|
314
|
+
options: z.object({
|
|
315
|
+
id: z
|
|
316
|
+
.string()
|
|
317
|
+
.optional()
|
|
318
|
+
.describe('遷移 ID(transitions コマンドで確認)'),
|
|
319
|
+
name: z
|
|
320
|
+
.string()
|
|
321
|
+
.optional()
|
|
322
|
+
.describe('遷移名の部分一致(例: "In Progress", "完了")。--id 未指定時に使用'),
|
|
323
|
+
...authOptions.shape,
|
|
324
|
+
}),
|
|
325
|
+
output: z.any(),
|
|
326
|
+
async run(c) {
|
|
327
|
+
const key = parseIssueKey(c.args.ref);
|
|
328
|
+
const creds = resolveCredentials(c.options);
|
|
329
|
+
if (!c.options.id && !c.options.name) {
|
|
330
|
+
throw new Error('--id または --name を指定してください');
|
|
331
|
+
}
|
|
332
|
+
let transitionId = c.options.id;
|
|
333
|
+
if (!transitionId) {
|
|
334
|
+
const res = await jiraRequest(creds, `/issue/${encodeURIComponent(key)}/transitions`);
|
|
335
|
+
const nameLower = c.options.name.toLowerCase();
|
|
336
|
+
const found = res.transitions.find((t) => t.name.toLowerCase().includes(nameLower));
|
|
337
|
+
if (!found) {
|
|
338
|
+
const names = res.transitions.map((t) => `"${t.name}"(id=${t.id})`).join(', ');
|
|
339
|
+
throw new Error(`遷移が見つかりません: "${c.options.name}". 候補: ${names}`);
|
|
340
|
+
}
|
|
341
|
+
transitionId = found.id;
|
|
342
|
+
}
|
|
343
|
+
await jiraRequest(creds, `/issue/${encodeURIComponent(key)}/transitions`, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
body: JSON.stringify({ transition: { id: transitionId } }),
|
|
346
|
+
});
|
|
347
|
+
const full = { ok: true, key, transitionId };
|
|
348
|
+
return finalizeCompactOutput(c, full, (data) => ({
|
|
349
|
+
ok: true,
|
|
350
|
+
k: data.key,
|
|
351
|
+
tid: data.transitionId,
|
|
352
|
+
}));
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
.command('comments', {
|
|
356
|
+
description: '課題のコメント一覧(キーまたは browse URL)',
|
|
357
|
+
args: z.object({
|
|
358
|
+
ref: z.string().describe('課題キー(WEC-41)または課題 URL'),
|
|
359
|
+
}),
|
|
360
|
+
options: z.object({
|
|
361
|
+
limit: z.coerce.number().int().min(1).max(100).default(50).describe('最大件数'),
|
|
362
|
+
...authOptions.shape,
|
|
363
|
+
}),
|
|
364
|
+
output: z.any(),
|
|
365
|
+
async run(c) {
|
|
366
|
+
const key = parseIssueKey(c.args.ref);
|
|
367
|
+
const creds = resolveCredentials(c.options);
|
|
368
|
+
const res = await jiraRequest(creds, `/issue/${encodeURIComponent(key)}/comment?maxResults=${c.options.limit}`);
|
|
369
|
+
const full = {
|
|
370
|
+
key,
|
|
371
|
+
total: res.total,
|
|
372
|
+
comments: res.comments.map((co) => ({
|
|
373
|
+
id: co.id,
|
|
374
|
+
author: co.author?.displayName ?? '',
|
|
375
|
+
created: co.created,
|
|
376
|
+
body: typeof co.body === 'string'
|
|
377
|
+
? co.body.trim()
|
|
378
|
+
: co.body != null && typeof co.body === 'object'
|
|
379
|
+
? plainTextFromAdf(co.body).trim()
|
|
380
|
+
: '',
|
|
381
|
+
})),
|
|
382
|
+
};
|
|
383
|
+
return finalizeCompactOutput(c, full, (data) => slimComments(data, true));
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
.command('comment', {
|
|
387
|
+
description: 'コメントを追加(メンション: @[accountId] または @[email:...])',
|
|
388
|
+
args: z.object({
|
|
389
|
+
ref: z.string().describe('課題キーまたは URL'),
|
|
390
|
+
}),
|
|
391
|
+
options: z.object({
|
|
392
|
+
body: z.string().min(1).describe('本文(改行可・@[...] メンション可)'),
|
|
393
|
+
...authOptions.shape,
|
|
394
|
+
}),
|
|
395
|
+
output: z.any(),
|
|
396
|
+
async run(c) {
|
|
397
|
+
const key = parseIssueKey(c.args.ref);
|
|
398
|
+
const creds = resolveCredentials(c.options);
|
|
399
|
+
const body = await descriptionToAdf(creds, c.options.body);
|
|
400
|
+
const created = await jiraRequest(creds, `/issue/${encodeURIComponent(key)}/comment`, {
|
|
401
|
+
method: 'POST',
|
|
402
|
+
body: JSON.stringify({ body }),
|
|
403
|
+
});
|
|
404
|
+
const full = { ok: true, key, id: created.id, self: created.self };
|
|
405
|
+
return finalizeCompactOutput(c, full, (data) => ({ ok: true, k: data.key, id: data.id }));
|
|
406
|
+
},
|
|
407
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** issue search でよく使う fields(一覧取得時) */
|
|
2
|
+
export declare const ISSUE_LIST_FIELDS: readonly ["summary", "status", "assignee", "reporter", "issuetype", "priority", "created", "updated", "labels", "parent"];
|
|
3
|
+
/** issue get で取得する fields(ISSUE_LIST_FIELDS + 詳細) */
|
|
4
|
+
export declare const ISSUE_DETAIL_FIELDS: readonly ["summary", "status", "assignee", "reporter", "issuetype", "priority", "created", "updated", "labels", "parent", "description", "components", "duedate"];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** issue search でよく使う fields(一覧取得時) */
|
|
2
|
+
export const ISSUE_LIST_FIELDS = [
|
|
3
|
+
'summary',
|
|
4
|
+
'status',
|
|
5
|
+
'assignee',
|
|
6
|
+
'reporter',
|
|
7
|
+
'issuetype',
|
|
8
|
+
'priority',
|
|
9
|
+
'created',
|
|
10
|
+
'updated',
|
|
11
|
+
'labels',
|
|
12
|
+
'parent',
|
|
13
|
+
];
|
|
14
|
+
/** issue get で取得する fields(ISSUE_LIST_FIELDS + 詳細) */
|
|
15
|
+
export const ISSUE_DETAIL_FIELDS = [
|
|
16
|
+
...ISSUE_LIST_FIELDS,
|
|
17
|
+
'description',
|
|
18
|
+
'components',
|
|
19
|
+
'duedate',
|
|
20
|
+
];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { JiraCredentials } from './config.js';
|
|
2
|
+
export declare function jiraRequest<T = unknown>(env: JiraCredentials, path: string, init?: RequestInit): Promise<T>;
|
|
3
|
+
type IssueFields = {
|
|
4
|
+
summary?: string;
|
|
5
|
+
status?: {
|
|
6
|
+
name?: string;
|
|
7
|
+
};
|
|
8
|
+
assignee?: {
|
|
9
|
+
displayName?: string;
|
|
10
|
+
} | null;
|
|
11
|
+
reporter?: {
|
|
12
|
+
displayName?: string;
|
|
13
|
+
} | null;
|
|
14
|
+
issuetype?: {
|
|
15
|
+
name?: string;
|
|
16
|
+
};
|
|
17
|
+
priority?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
};
|
|
20
|
+
description?: unknown;
|
|
21
|
+
created?: string;
|
|
22
|
+
updated?: string;
|
|
23
|
+
labels?: string[];
|
|
24
|
+
components?: Array<{
|
|
25
|
+
name?: string;
|
|
26
|
+
}>;
|
|
27
|
+
duedate?: string | null;
|
|
28
|
+
parent?: {
|
|
29
|
+
key?: string;
|
|
30
|
+
fields?: {
|
|
31
|
+
summary?: string;
|
|
32
|
+
status?: {
|
|
33
|
+
name?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
} | null;
|
|
37
|
+
};
|
|
38
|
+
export type IssueSummary = {
|
|
39
|
+
key: string;
|
|
40
|
+
id: string;
|
|
41
|
+
self: string;
|
|
42
|
+
summary: string;
|
|
43
|
+
status: string;
|
|
44
|
+
assignee: string | null;
|
|
45
|
+
reporter: string | null;
|
|
46
|
+
issuetype: string;
|
|
47
|
+
priority: string | null;
|
|
48
|
+
/** description を ADF から抜いたテキスト(無ければ null) */
|
|
49
|
+
description: string | null;
|
|
50
|
+
created: string | null;
|
|
51
|
+
updated: string | null;
|
|
52
|
+
labels: string[];
|
|
53
|
+
components: string[];
|
|
54
|
+
duedate: string | null;
|
|
55
|
+
parent: {
|
|
56
|
+
key: string;
|
|
57
|
+
summary: string;
|
|
58
|
+
status: string;
|
|
59
|
+
} | null;
|
|
60
|
+
};
|
|
61
|
+
export declare function toIssueSummary(raw: {
|
|
62
|
+
key: string;
|
|
63
|
+
id: string;
|
|
64
|
+
self: string;
|
|
65
|
+
fields?: IssueFields;
|
|
66
|
+
}): IssueSummary;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { plainTextFromAdf } from './adf.js';
|
|
3
|
+
import { normalizeJiraBaseUrl } from './jira-env.js';
|
|
4
|
+
function formatJiraError(status, data) {
|
|
5
|
+
if (data && typeof data === 'object' && 'errorMessages' in data) {
|
|
6
|
+
const msgs = data.errorMessages;
|
|
7
|
+
if (Array.isArray(msgs) && msgs.length)
|
|
8
|
+
return msgs.join('; ');
|
|
9
|
+
}
|
|
10
|
+
if (data && typeof data === 'object' && 'errors' in data) {
|
|
11
|
+
const err = data.errors;
|
|
12
|
+
if (err && typeof err === 'object') {
|
|
13
|
+
return Object.entries(err)
|
|
14
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
15
|
+
.join('; ');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return `HTTP ${status}`;
|
|
19
|
+
}
|
|
20
|
+
export async function jiraRequest(env, path, init) {
|
|
21
|
+
const base = normalizeJiraBaseUrl(env.host);
|
|
22
|
+
const url = `${base}/rest/api/3${path.startsWith('/') ? path : `/${path}`}`;
|
|
23
|
+
const token = Buffer.from(`${env.email}:${env.apiToken}`).toString('base64');
|
|
24
|
+
const headers = new Headers(init?.headers);
|
|
25
|
+
headers.set('Authorization', `Basic ${token}`);
|
|
26
|
+
headers.set('Accept', 'application/json');
|
|
27
|
+
if (init?.body !== undefined && !headers.has('Content-Type')) {
|
|
28
|
+
headers.set('Content-Type', 'application/json');
|
|
29
|
+
}
|
|
30
|
+
const res = await fetch(url, { ...init, headers });
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
let data = null;
|
|
33
|
+
if (text) {
|
|
34
|
+
try {
|
|
35
|
+
data = JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
data = { raw: text };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`Jira API ${res.status}: ${formatJiraError(res.status, data)}`);
|
|
43
|
+
}
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
export function toIssueSummary(raw) {
|
|
47
|
+
const f = raw.fields ?? {};
|
|
48
|
+
const descRaw = f.description;
|
|
49
|
+
const description = descRaw != null && typeof descRaw === 'object'
|
|
50
|
+
? plainTextFromAdf(descRaw).trim() || null
|
|
51
|
+
: null;
|
|
52
|
+
const parent = f.parent?.key != null
|
|
53
|
+
? {
|
|
54
|
+
key: f.parent.key,
|
|
55
|
+
summary: f.parent.fields?.summary ?? '',
|
|
56
|
+
status: f.parent.fields?.status?.name ?? '',
|
|
57
|
+
}
|
|
58
|
+
: null;
|
|
59
|
+
return {
|
|
60
|
+
key: raw.key,
|
|
61
|
+
id: raw.id,
|
|
62
|
+
self: raw.self,
|
|
63
|
+
summary: f.summary ?? '',
|
|
64
|
+
status: f.status?.name ?? '',
|
|
65
|
+
assignee: f.assignee?.displayName ?? null,
|
|
66
|
+
reporter: f.reporter?.displayName ?? null,
|
|
67
|
+
issuetype: f.issuetype?.name ?? '',
|
|
68
|
+
priority: f.priority?.name ?? null,
|
|
69
|
+
description,
|
|
70
|
+
created: f.created ?? null,
|
|
71
|
+
updated: f.updated ?? null,
|
|
72
|
+
labels: f.labels ?? [],
|
|
73
|
+
components: (f.components ?? []).map((c) => c.name ?? '').filter(Boolean),
|
|
74
|
+
duedate: f.duedate ?? null,
|
|
75
|
+
parent,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
/** 環境変数スキーマ(各コマンドで共有) */
|
|
3
|
+
export declare const jiraEnvSchema: z.ZodObject<{
|
|
4
|
+
JIRA_HOST: z.ZodString;
|
|
5
|
+
JIRA_EMAIL: z.ZodString;
|
|
6
|
+
JIRA_API_TOKEN: z.ZodString;
|
|
7
|
+
JIRA_CLI_COMPACT: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type JiraEnv = z.infer<typeof jiraEnvSchema>;
|
|
10
|
+
export declare function normalizeJiraBaseUrl(host: string): string;
|
package/dist/jira-env.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'incur';
|
|
2
|
+
/** 環境変数スキーマ(各コマンドで共有) */
|
|
3
|
+
export const jiraEnvSchema = z.object({
|
|
4
|
+
JIRA_HOST: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe('Jira サイトホスト(例: your-company.atlassian.net、https は不要)'),
|
|
8
|
+
JIRA_EMAIL: z.string().email().describe('Atlassian アカウントのメール'),
|
|
9
|
+
JIRA_API_TOKEN: z.string().min(1).describe('API トークン(Atlassian アカウント設定から発行)'),
|
|
10
|
+
JIRA_CLI_COMPACT: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('1/true=常に圧縮出力、0/false=常にフル、未設定=パイプ/非TTY のときだけ圧縮(エージェント向けトークン削減)'),
|
|
14
|
+
});
|
|
15
|
+
export function normalizeJiraBaseUrl(host) {
|
|
16
|
+
let h = host.trim();
|
|
17
|
+
if (h.startsWith('https://'))
|
|
18
|
+
h = h.slice('https://'.length);
|
|
19
|
+
else if (h.startsWith('http://'))
|
|
20
|
+
h = h.slice('http://'.length);
|
|
21
|
+
h = h.replace(/\/$/, '');
|
|
22
|
+
return `https://${h}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* コマンド結果を NDJSON(1 行 1 JSON)にする。
|
|
3
|
+
* 配列系は 1 行ずつ、先頭にメタ行(`_: "meta"`)を付ける場合あり。
|
|
4
|
+
*/
|
|
5
|
+
function collectionToJsonl(object, collectionKey, rows) {
|
|
6
|
+
const meta = { ...object };
|
|
7
|
+
delete meta[collectionKey];
|
|
8
|
+
const lines = [];
|
|
9
|
+
if (Object.keys(meta).length > 0) {
|
|
10
|
+
lines.push(JSON.stringify({ _: 'meta', ...meta }));
|
|
11
|
+
}
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
lines.push(JSON.stringify(row));
|
|
14
|
+
}
|
|
15
|
+
return `${lines.join('\n')}\n`;
|
|
16
|
+
}
|
|
17
|
+
export function valueToJsonlLines(data) {
|
|
18
|
+
if (data == null)
|
|
19
|
+
return '\n';
|
|
20
|
+
if (typeof data !== 'object') {
|
|
21
|
+
return `${JSON.stringify(data)}\n`;
|
|
22
|
+
}
|
|
23
|
+
const o = data;
|
|
24
|
+
if ('issue' in o && o.issue != null && typeof o.issue === 'object') {
|
|
25
|
+
return `${JSON.stringify(o.issue)}\n`;
|
|
26
|
+
}
|
|
27
|
+
const issuesKey = 'issues' in o ? 'issues' : 'i' in o ? 'i' : null;
|
|
28
|
+
if (issuesKey && Array.isArray(o[issuesKey])) {
|
|
29
|
+
return collectionToJsonl(o, issuesKey, o[issuesKey]);
|
|
30
|
+
}
|
|
31
|
+
if ('comments' in o && Array.isArray(o.comments)) {
|
|
32
|
+
return collectionToJsonl(o, 'comments', o.comments);
|
|
33
|
+
}
|
|
34
|
+
if ('c' in o && Array.isArray(o.c)) {
|
|
35
|
+
return collectionToJsonl(o, 'c', o.c);
|
|
36
|
+
}
|
|
37
|
+
if ('projects' in o && Array.isArray(o.projects)) {
|
|
38
|
+
return collectionToJsonl(o, 'projects', o.projects);
|
|
39
|
+
}
|
|
40
|
+
if ('p' in o && Array.isArray(o.p)) {
|
|
41
|
+
return collectionToJsonl(o, 'p', o.p);
|
|
42
|
+
}
|
|
43
|
+
if ('users' in o && Array.isArray(o.users)) {
|
|
44
|
+
return collectionToJsonl(o, 'users', o.users);
|
|
45
|
+
}
|
|
46
|
+
if ('u' in o && Array.isArray(o.u)) {
|
|
47
|
+
return collectionToJsonl(o, 'u', o.u);
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(data)) {
|
|
50
|
+
return `${data.map((x) => JSON.stringify(x)).join('\n')}\n`;
|
|
51
|
+
}
|
|
52
|
+
return `${JSON.stringify(data)}\n`;
|
|
53
|
+
}
|
|
54
|
+
/** `--format jsonl` のとき NDJSON 文字列を返す(incur はスカラー文字列をそのまま stdout へ出す) */
|
|
55
|
+
export function outJsonlIfNeeded(data, format) {
|
|
56
|
+
return format === 'jsonl' ? valueToJsonlLines(data) : data;
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadDotenvFiles(): void;
|