@jackwener/opencli 1.7.22 → 1.8.1
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 +35 -194
- package/README.zh-CN.md +42 -260
- package/cli-manifest.json +8160 -4392
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +13 -6
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +7 -3
- package/clis/twitter/search.test.js +41 -0
- package/clis/twitter/shared.js +155 -0
- package/clis/twitter/shared.test.js +465 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +98 -11
- package/clis/weread/search.js +32 -9
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +166 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +252 -2
- package/clis/xiaohongshu/creator-notes.test.js +90 -1
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +2 -19
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +17 -16
- package/clis/zhihu/collection.test.js +50 -3
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -17
- package/clis/zhihu/question.test.js +113 -11
- package/clis/zhihu/search.js +195 -43
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import { __test__ as jiraSharedTest } from './shared.js';
|
|
5
|
+
import './issue.js';
|
|
6
|
+
import './search.js';
|
|
7
|
+
import './comments.js';
|
|
8
|
+
import './attachments.js';
|
|
9
|
+
import './links.js';
|
|
10
|
+
|
|
11
|
+
const ENV_KEYS = [
|
|
12
|
+
'ATLASSIAN_JIRA_BASE_URL',
|
|
13
|
+
'ATLASSIAN_DEPLOYMENT',
|
|
14
|
+
'ATLASSIAN_EMAIL',
|
|
15
|
+
'ATLASSIAN_API_TOKEN',
|
|
16
|
+
'ATLASSIAN_USERNAME',
|
|
17
|
+
'ATLASSIAN_PASSWORD',
|
|
18
|
+
'ATLASSIAN_PAT',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function clearEnv() {
|
|
22
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setCloudEnv() {
|
|
26
|
+
clearEnv();
|
|
27
|
+
process.env.ATLASSIAN_JIRA_BASE_URL = 'https://team.atlassian.net';
|
|
28
|
+
process.env.ATLASSIAN_DEPLOYMENT = 'cloud';
|
|
29
|
+
process.env.ATLASSIAN_EMAIL = 'bot@example.com';
|
|
30
|
+
process.env.ATLASSIAN_API_TOKEN = 'secret';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function jsonResponse(body) {
|
|
34
|
+
return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
clearEnv();
|
|
39
|
+
vi.unstubAllGlobals();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('jira commands', () => {
|
|
43
|
+
it('registers non-browser REST commands', () => {
|
|
44
|
+
for (const name of ['issue', 'search', 'comments', 'attachments', 'links']) {
|
|
45
|
+
const cmd = getRegistry().get(`jira/${name}`);
|
|
46
|
+
expect(cmd).toBeDefined();
|
|
47
|
+
expect(cmd.browser).toBe(false);
|
|
48
|
+
expect(cmd.strategy).toBe('public');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('normalizes a Cloud issue into agent-friendly context', async () => {
|
|
53
|
+
setCloudEnv();
|
|
54
|
+
vi.stubGlobal('fetch', vi.fn(async (url) => {
|
|
55
|
+
expect(String(url)).toContain('/rest/api/3/issue/PROJ-1?');
|
|
56
|
+
return jsonResponse({
|
|
57
|
+
id: '10001',
|
|
58
|
+
key: 'PROJ-1',
|
|
59
|
+
fields: {
|
|
60
|
+
summary: 'Checkout fails',
|
|
61
|
+
issuetype: { name: 'Bug' },
|
|
62
|
+
status: { name: 'In Progress' },
|
|
63
|
+
priority: { name: 'High' },
|
|
64
|
+
labels: ['prod'],
|
|
65
|
+
description: {
|
|
66
|
+
type: 'doc',
|
|
67
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Payment fails' }] }],
|
|
68
|
+
},
|
|
69
|
+
comment: {
|
|
70
|
+
total: 1,
|
|
71
|
+
comments: [{
|
|
72
|
+
id: 'c1',
|
|
73
|
+
author: { displayName: 'Alice' },
|
|
74
|
+
created: '2026-05-01T00:00:00.000+0000',
|
|
75
|
+
body: { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Needs RCA' }] }] },
|
|
76
|
+
}],
|
|
77
|
+
},
|
|
78
|
+
attachment: [{ id: 'a1', filename: 'log.txt', mimeType: 'text/plain', size: 12, content: 'https://team.atlassian.net/secure/attachment/a1/log.txt' }],
|
|
79
|
+
issuelinks: [{ type: { name: 'Blocks' }, outwardIssue: { key: 'PROJ-2' } }],
|
|
80
|
+
fixVersions: [{ name: '1.2.3' }],
|
|
81
|
+
created: '2026-05-01T00:00:00.000+0000',
|
|
82
|
+
updated: '2026-05-02T00:00:00.000+0000',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}));
|
|
86
|
+
const cmd = getRegistry().get('jira/issue');
|
|
87
|
+
const rows = await cmd.func({ key: 'proj-1' });
|
|
88
|
+
expect(rows[0]).toMatchObject({
|
|
89
|
+
key: 'PROJ-1',
|
|
90
|
+
summary: 'Checkout fails',
|
|
91
|
+
issueType: 'Bug',
|
|
92
|
+
status: 'In Progress',
|
|
93
|
+
labels: ['prod'],
|
|
94
|
+
url: 'https://team.atlassian.net/browse/PROJ-1',
|
|
95
|
+
});
|
|
96
|
+
expect(rows[0].description.markdown).toBe('Payment fails');
|
|
97
|
+
expect(rows[0].comments[0].markdown).toBe('Needs RCA');
|
|
98
|
+
expect(rows[0].attachments[0].filename).toBe('log.txt');
|
|
99
|
+
expect(rows[0].linkedIssues[0]).toEqual({ key: 'PROJ-2', type: 'Blocks', direction: 'outward' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('fails typed when Jira issue payload is missing stable issue identity', async () => {
|
|
103
|
+
setCloudEnv();
|
|
104
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ fields: { summary: 'No key' } })));
|
|
105
|
+
const cmd = getRegistry().get('jira/issue');
|
|
106
|
+
await expect(cmd.func({ key: 'PROJ-1' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('fails typed when Jira issue nested collections have malformed shapes', async () => {
|
|
110
|
+
setCloudEnv();
|
|
111
|
+
const cmd = getRegistry().get('jira/issue');
|
|
112
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
113
|
+
id: '10001',
|
|
114
|
+
key: 'PROJ-1',
|
|
115
|
+
fields: {
|
|
116
|
+
summary: 'Checkout fails',
|
|
117
|
+
labels: [],
|
|
118
|
+
comment: { total: 1, comments: { id: 'c1' } },
|
|
119
|
+
attachment: [],
|
|
120
|
+
issuelinks: [],
|
|
121
|
+
},
|
|
122
|
+
})));
|
|
123
|
+
await expect(cmd.func({ key: 'PROJ-1' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
124
|
+
|
|
125
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
126
|
+
id: '10001',
|
|
127
|
+
key: 'PROJ-1',
|
|
128
|
+
fields: {
|
|
129
|
+
summary: 'Checkout fails',
|
|
130
|
+
labels: [],
|
|
131
|
+
comment: { total: 0, comments: [] },
|
|
132
|
+
attachment: { id: 'a1' },
|
|
133
|
+
issuelinks: [],
|
|
134
|
+
},
|
|
135
|
+
})));
|
|
136
|
+
await expect(cmd.func({ key: 'PROJ-1' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
137
|
+
|
|
138
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
139
|
+
id: '10001',
|
|
140
|
+
key: 'PROJ-1',
|
|
141
|
+
fields: {
|
|
142
|
+
summary: 'Checkout fails',
|
|
143
|
+
labels: [],
|
|
144
|
+
comment: null,
|
|
145
|
+
attachment: [],
|
|
146
|
+
issuelinks: [],
|
|
147
|
+
},
|
|
148
|
+
})));
|
|
149
|
+
await expect(cmd.func({ key: 'PROJ-1' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
150
|
+
|
|
151
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
152
|
+
id: '10001',
|
|
153
|
+
key: 'PROJ-1',
|
|
154
|
+
fields: {
|
|
155
|
+
summary: 'Checkout fails',
|
|
156
|
+
labels: [],
|
|
157
|
+
comment: { total: 0, comments: [] },
|
|
158
|
+
attachment: null,
|
|
159
|
+
issuelinks: [],
|
|
160
|
+
},
|
|
161
|
+
})));
|
|
162
|
+
await expect(cmd.func({ key: 'PROJ-1' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('rejects invalid Jira issue keys before remote requests', async () => {
|
|
166
|
+
expect(() => jiraSharedTest.requireIssueKey('notakey')).toThrow(/Invalid Jira issue key/);
|
|
167
|
+
const fetchMock = vi.fn();
|
|
168
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
169
|
+
const cmd = getRegistry().get('jira/issue');
|
|
170
|
+
await expect(cmd.func({ key: 'notakey' })).rejects.toMatchObject({ code: 'ARGUMENT' });
|
|
171
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('fetches full comments when issue inline comments are truncated', async () => {
|
|
175
|
+
setCloudEnv();
|
|
176
|
+
const fetchMock = vi.fn(async (url) => {
|
|
177
|
+
const href = String(url);
|
|
178
|
+
if (href.includes('/comment?')) {
|
|
179
|
+
return jsonResponse({
|
|
180
|
+
comments: [
|
|
181
|
+
{ id: 'c1', author: { displayName: 'Alice' }, created: '2026-05-01', body: 'one' },
|
|
182
|
+
{ id: 'c2', author: { displayName: 'Bob' }, created: '2026-05-02', body: 'two' },
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return jsonResponse({
|
|
187
|
+
id: '10001',
|
|
188
|
+
key: 'PROJ-1',
|
|
189
|
+
fields: {
|
|
190
|
+
summary: 'Checkout fails',
|
|
191
|
+
labels: [],
|
|
192
|
+
comment: {
|
|
193
|
+
total: 2,
|
|
194
|
+
comments: [{ id: 'c1', author: { displayName: 'Alice' }, created: '2026-05-01', body: 'one' }],
|
|
195
|
+
},
|
|
196
|
+
attachment: [],
|
|
197
|
+
issuelinks: [],
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
202
|
+
const cmd = getRegistry().get('jira/issue');
|
|
203
|
+
const rows = await cmd.func({ key: 'PROJ-1', 'comments-limit': 10 });
|
|
204
|
+
expect(rows[0].comments.map((comment) => comment.id)).toEqual(['c1', 'c2']);
|
|
205
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('uses Data Center search endpoint and payload', async () => {
|
|
209
|
+
clearEnv();
|
|
210
|
+
process.env.ATLASSIAN_JIRA_BASE_URL = 'https://jira.example.com';
|
|
211
|
+
process.env.ATLASSIAN_DEPLOYMENT = 'datacenter';
|
|
212
|
+
process.env.ATLASSIAN_USERNAME = 'bot';
|
|
213
|
+
process.env.ATLASSIAN_PASSWORD = 'secret';
|
|
214
|
+
const fetchMock = vi.fn(async (url, init) => {
|
|
215
|
+
expect(String(url)).toBe('https://jira.example.com/rest/api/2/search');
|
|
216
|
+
expect(init.method).toBe('POST');
|
|
217
|
+
expect(JSON.parse(init.body)).toMatchObject({
|
|
218
|
+
jql: 'project = PROJ',
|
|
219
|
+
startAt: 0,
|
|
220
|
+
maxResults: 5,
|
|
221
|
+
});
|
|
222
|
+
return jsonResponse({
|
|
223
|
+
issues: [{
|
|
224
|
+
id: '1',
|
|
225
|
+
key: 'PROJ-1',
|
|
226
|
+
fields: {
|
|
227
|
+
summary: 'Task',
|
|
228
|
+
status: { name: 'Done' },
|
|
229
|
+
updated: '2026-05-03T00:00:00.000+0000',
|
|
230
|
+
},
|
|
231
|
+
}],
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
235
|
+
const cmd = getRegistry().get('jira/search');
|
|
236
|
+
const rows = await cmd.func({ jql: 'project = PROJ', limit: 5 });
|
|
237
|
+
expect(rows).toEqual([
|
|
238
|
+
expect.objectContaining({ key: 'PROJ-1', summary: 'Task', status: 'Done' }),
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('separates Jira search empty results from malformed search payloads', async () => {
|
|
243
|
+
setCloudEnv();
|
|
244
|
+
const cmd = getRegistry().get('jira/search');
|
|
245
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ issues: [] })));
|
|
246
|
+
await expect(cmd.func({ jql: 'project = NONE' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
247
|
+
|
|
248
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ values: [] })));
|
|
249
|
+
await expect(cmd.func({ jql: 'project = PROJ' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('maps rendered comments to Markdown', async () => {
|
|
253
|
+
setCloudEnv();
|
|
254
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
255
|
+
comments: [{
|
|
256
|
+
id: 'c1',
|
|
257
|
+
author: { displayName: 'Bob' },
|
|
258
|
+
created: '2026-05-01',
|
|
259
|
+
renderedBody: '<p>Fixed<br/>Ready for QA</p>',
|
|
260
|
+
}],
|
|
261
|
+
})));
|
|
262
|
+
const cmd = getRegistry().get('jira/comments');
|
|
263
|
+
const rows = await cmd.func({ key: 'PROJ-1', limit: 1 });
|
|
264
|
+
expect(rows[0]).toMatchObject({ id: 'c1', author: 'Bob' });
|
|
265
|
+
expect(rows[0].markdown).toContain('Fixed');
|
|
266
|
+
expect(rows[0].markdown).toContain('Ready for QA');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('fails typed when Jira comment rows lack stable comment ids', async () => {
|
|
270
|
+
setCloudEnv();
|
|
271
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
272
|
+
comments: [{ author: { displayName: 'Bob' }, created: '2026-05-01', body: 'body' }],
|
|
273
|
+
})));
|
|
274
|
+
const cmd = getRegistry().get('jira/comments');
|
|
275
|
+
await expect(cmd.func({ key: 'PROJ-1', limit: 1 })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('separates no Jira attachments from malformed attachment payloads', async () => {
|
|
279
|
+
setCloudEnv();
|
|
280
|
+
const cmd = getRegistry().get('jira/attachments');
|
|
281
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ id: '1', key: 'PROJ-1', fields: { attachment: [] } })));
|
|
282
|
+
await expect(cmd.func({ key: 'PROJ-1' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
283
|
+
|
|
284
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ id: '1', key: 'PROJ-1', fields: {} })));
|
|
285
|
+
await expect(cmd.func({ key: 'PROJ-1' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { fetchComments, jiraConfig, jiraRowsOrEmpty, normalizeComment, parseJiraLimit, requireIssueKey } from './shared.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'jira',
|
|
6
|
+
name: 'comments',
|
|
7
|
+
access: 'read',
|
|
8
|
+
description: 'Jira issue comments as Markdown',
|
|
9
|
+
domain: 'atlassian.net',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'key', positional: true, required: true, help: 'Jira issue key, e.g. PROJ-123' },
|
|
14
|
+
{ name: 'limit', type: 'int', default: 50, help: 'Max comments to return (1-100)' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['id', 'author', 'created', 'updated', 'markdown'],
|
|
17
|
+
func: async (args) => {
|
|
18
|
+
const key = requireIssueKey(args.key);
|
|
19
|
+
const config = jiraConfig();
|
|
20
|
+
const limit = parseJiraLimit(args.limit, 50, 100);
|
|
21
|
+
const comments = await fetchComments(config, key, limit);
|
|
22
|
+
return jiraRowsOrEmpty(
|
|
23
|
+
comments.map(normalizeComment),
|
|
24
|
+
`jira comments ${key}`,
|
|
25
|
+
`Jira issue ${key} has no comments.`,
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { fetchComments, fetchIssue, jiraConfig, normalizeJiraIssue, requireIssueKey } from './shared.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'jira',
|
|
6
|
+
name: 'issue',
|
|
7
|
+
access: 'read',
|
|
8
|
+
description: 'Jira issue detail normalized for agents (description, comments, attachments, links)',
|
|
9
|
+
domain: 'atlassian.net',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [
|
|
13
|
+
{ name: 'key', positional: true, required: true, help: 'Jira issue key, e.g. PROJ-123' },
|
|
14
|
+
{ name: 'comments-limit', type: 'int', default: 100, help: 'Max comments to include (1-100)' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['key', 'summary', 'issueType', 'status', 'priority', 'assignee', 'updated', 'url'],
|
|
17
|
+
func: async (args) => {
|
|
18
|
+
const key = requireIssueKey(args.key);
|
|
19
|
+
const config = jiraConfig();
|
|
20
|
+
const issue = await fetchIssue(config, key);
|
|
21
|
+
const inlineComments = issue?.fields?.comment?.comments;
|
|
22
|
+
const total = Number(issue?.fields?.comment?.total ?? inlineComments?.length ?? 0);
|
|
23
|
+
const comments = total > (inlineComments?.length ?? 0)
|
|
24
|
+
? await fetchComments(config, key, args['comments-limit'])
|
|
25
|
+
: inlineComments;
|
|
26
|
+
return [normalizeJiraIssue(issue, config, { comments })];
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { fetchIssue, jiraConfig, jiraRowsOrEmpty, normalizeIssueLink, requireIssueKey } from './shared.js';
|
|
3
|
+
import { requirePayloadArray } from '../_atlassian/shared.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'jira',
|
|
7
|
+
name: 'links',
|
|
8
|
+
access: 'read',
|
|
9
|
+
description: 'Jira issue links',
|
|
10
|
+
domain: 'atlassian.net',
|
|
11
|
+
strategy: Strategy.PUBLIC,
|
|
12
|
+
browser: false,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'key', positional: true, required: true, help: 'Jira issue key, e.g. PROJ-123' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['key', 'type', 'direction'],
|
|
17
|
+
func: async (args) => {
|
|
18
|
+
const key = requireIssueKey(args.key);
|
|
19
|
+
const config = jiraConfig();
|
|
20
|
+
const issue = await fetchIssue(config, key, ['issuelinks']);
|
|
21
|
+
const links = requirePayloadArray(issue.fields?.issuelinks, `jira links ${key}`);
|
|
22
|
+
return jiraRowsOrEmpty(
|
|
23
|
+
links.map(normalizeIssueLink),
|
|
24
|
+
`jira links ${key}`,
|
|
25
|
+
`Jira issue ${key} has no linked issues.`,
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { jiraConfig, issueSummaryRow, jiraRowsOrEmpty, parseJiraLimit } from './shared.js';
|
|
3
|
+
import { atlassianRequest, requirePayloadArray, requirePayloadObject, requireString } from '../_atlassian/shared.js';
|
|
4
|
+
|
|
5
|
+
function searchPath(config) {
|
|
6
|
+
return config.deployment === 'cloud' ? '/rest/api/3/search/jql' : '/rest/api/2/search';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function searchPayload(config, jql, limit) {
|
|
10
|
+
const fields = ['summary', 'issuetype', 'status', 'priority', 'assignee', 'updated'];
|
|
11
|
+
if (config.deployment === 'cloud') return { jql, maxResults: limit, fields };
|
|
12
|
+
return { jql, startAt: 0, maxResults: limit, fields };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
cli({
|
|
16
|
+
site: 'jira',
|
|
17
|
+
name: 'search',
|
|
18
|
+
access: 'read',
|
|
19
|
+
description: 'Search Jira issues with JQL',
|
|
20
|
+
domain: 'atlassian.net',
|
|
21
|
+
strategy: Strategy.PUBLIC,
|
|
22
|
+
browser: false,
|
|
23
|
+
args: [
|
|
24
|
+
{ name: 'jql', positional: true, required: true, help: 'JQL query, e.g. "project = PROJ order by updated desc"' },
|
|
25
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max issues to return (1-100)' },
|
|
26
|
+
],
|
|
27
|
+
columns: ['key', 'summary', 'issueType', 'status', 'priority', 'assignee', 'updated', 'url'],
|
|
28
|
+
func: async (args) => {
|
|
29
|
+
const config = jiraConfig();
|
|
30
|
+
const jql = requireString(args.jql, 'JQL');
|
|
31
|
+
const limit = parseJiraLimit(args.limit, 20, 100);
|
|
32
|
+
const data = await atlassianRequest(config, searchPath(config), {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
body: searchPayload(config, jql, limit),
|
|
35
|
+
label: 'jira search',
|
|
36
|
+
});
|
|
37
|
+
const payload = requirePayloadObject(data, 'jira search');
|
|
38
|
+
const issues = requirePayloadArray(payload.issues, 'jira search issues');
|
|
39
|
+
return jiraRowsOrEmpty(
|
|
40
|
+
issues.map((issue) => issueSummaryRow(issue, config)),
|
|
41
|
+
'jira search',
|
|
42
|
+
`No Jira issues matched "${jql}".`,
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const __test__ = { searchPath, searchPayload };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
adfToMarkdown,
|
|
3
|
+
atlassianRequest,
|
|
4
|
+
getJiraConfig,
|
|
5
|
+
htmlToMarkdown,
|
|
6
|
+
parseLimit,
|
|
7
|
+
queryString,
|
|
8
|
+
requireNonEmptyRows,
|
|
9
|
+
requirePayloadArray,
|
|
10
|
+
requirePayloadObject,
|
|
11
|
+
requirePayloadString,
|
|
12
|
+
requireString,
|
|
13
|
+
} from '../_atlassian/shared.js';
|
|
14
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ISSUE_FIELDS = [
|
|
17
|
+
'summary',
|
|
18
|
+
'issuetype',
|
|
19
|
+
'status',
|
|
20
|
+
'priority',
|
|
21
|
+
'labels',
|
|
22
|
+
'description',
|
|
23
|
+
'comment',
|
|
24
|
+
'attachment',
|
|
25
|
+
'issuelinks',
|
|
26
|
+
'fixVersions',
|
|
27
|
+
'versions',
|
|
28
|
+
'components',
|
|
29
|
+
'project',
|
|
30
|
+
'reporter',
|
|
31
|
+
'assignee',
|
|
32
|
+
'created',
|
|
33
|
+
'updated',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function jiraApiPrefix(config) {
|
|
37
|
+
return `/rest/api/${config.deployment === 'cloud' ? '3' : '2'}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function jiraApiPath(config, resource, params) {
|
|
41
|
+
return `${jiraApiPrefix(config)}${resource.startsWith('/') ? resource : `/${resource}`}${params ? queryString(params) : ''}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function jiraRequest(config, resource, options = {}) {
|
|
45
|
+
return atlassianRequest(config, jiraApiPath(config, resource, options.params), options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function configuredFieldNames() {
|
|
49
|
+
return {
|
|
50
|
+
acceptanceCriteria: process.env.ATLASSIAN_JIRA_ACCEPTANCE_FIELD?.trim() || '',
|
|
51
|
+
sprint: process.env.ATLASSIAN_JIRA_SPRINT_FIELD?.trim() || '',
|
|
52
|
+
storyPoints: process.env.ATLASSIAN_JIRA_STORY_POINTS_FIELD?.trim() || '',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function issueFields(extraFields = []) {
|
|
57
|
+
const configured = Object.values(configuredFieldNames()).filter(Boolean);
|
|
58
|
+
return [...new Set([...DEFAULT_ISSUE_FIELDS, ...configured, ...extraFields.filter(Boolean)])].join(',');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseJiraLimit(value, fallback = 20, max = 100) {
|
|
62
|
+
return parseLimit(value, fallback, max, 'jira limit');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function requireIssueKey(value) {
|
|
66
|
+
const key = requireString(value, 'Jira issue key');
|
|
67
|
+
if (!/^[A-Za-z][A-Za-z0-9_]+-\d+$/.test(key)) {
|
|
68
|
+
throw new ArgumentError(`Invalid Jira issue key: ${key}`, 'Expected a key like PROJECT-123.');
|
|
69
|
+
}
|
|
70
|
+
return key.toUpperCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function displayUser(user) {
|
|
74
|
+
if (!user || typeof user !== 'object') return '';
|
|
75
|
+
return String(user.displayName ?? user.name ?? user.emailAddress ?? user.accountId ?? '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function valueName(value) {
|
|
79
|
+
if (!value) return '';
|
|
80
|
+
if (typeof value === 'string') return value;
|
|
81
|
+
if (typeof value === 'object') return String(value.name ?? value.value ?? value.key ?? value.id ?? '');
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function valueNames(values) {
|
|
86
|
+
return Array.isArray(values) ? values.map(valueName).filter(Boolean) : [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function jiraBodyToMarkdown(raw, rendered) {
|
|
90
|
+
if (rendered) return htmlToMarkdown(rendered);
|
|
91
|
+
if (raw && typeof raw === 'object') return adfToMarkdown(raw);
|
|
92
|
+
if (typeof raw === 'string') return raw.trim();
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function normalizeComment(comment) {
|
|
97
|
+
const row = requirePayloadObject(comment, 'jira comment');
|
|
98
|
+
return {
|
|
99
|
+
id: requirePayloadString(row.id, 'comment id', 'jira comment'),
|
|
100
|
+
author: displayUser(row.author),
|
|
101
|
+
created: row.created ? String(row.created) : '',
|
|
102
|
+
updated: row.updated ? String(row.updated) : undefined,
|
|
103
|
+
markdown: jiraBodyToMarkdown(row.body, row.renderedBody),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function normalizeAttachment(attachment) {
|
|
108
|
+
const row = requirePayloadObject(attachment, 'jira attachment');
|
|
109
|
+
return {
|
|
110
|
+
id: requirePayloadString(row.id, 'attachment id', 'jira attachment'),
|
|
111
|
+
filename: requirePayloadString(row.filename, 'filename', 'jira attachment'),
|
|
112
|
+
mimeType: row.mimeType ? String(row.mimeType) : undefined,
|
|
113
|
+
size: row.size != null ? Number(row.size) : undefined,
|
|
114
|
+
url: requirePayloadString(row.content ?? row.self, 'attachment url', 'jira attachment'),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function normalizeIssueLink(link) {
|
|
119
|
+
const row = requirePayloadObject(link, 'jira issue link');
|
|
120
|
+
const outward = row.outwardIssue;
|
|
121
|
+
const inward = row.inwardIssue;
|
|
122
|
+
const issue = outward ?? inward ?? {};
|
|
123
|
+
const key = requirePayloadString(issue.key, 'linked issue key', 'jira issue link');
|
|
124
|
+
return {
|
|
125
|
+
key,
|
|
126
|
+
type: String(row.type?.name ?? (outward ? row.type?.outward : row.type?.inward) ?? ''),
|
|
127
|
+
direction: outward ? 'outward' : 'inward',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function customValueToMarkdown(value) {
|
|
132
|
+
if (!value) return '';
|
|
133
|
+
if (typeof value === 'string') return value.trim();
|
|
134
|
+
if (value && typeof value === 'object' && value.type === 'doc') return adfToMarkdown(value);
|
|
135
|
+
if (Array.isArray(value)) return value.map(valueName).filter(Boolean).join(', ');
|
|
136
|
+
return valueName(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function inlineComments(fields, key, options) {
|
|
140
|
+
if (options.comments !== undefined) return requirePayloadArray(options.comments, `jira issue ${key} comments`);
|
|
141
|
+
if (options.requireNestedCollections === false) return [];
|
|
142
|
+
const commentBlock = requirePayloadObject(fields.comment, `jira issue ${key} comment field`);
|
|
143
|
+
return requirePayloadArray(commentBlock.comments, `jira issue ${key} comment field comments`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function normalizeJiraIssue(issue, config, options = {}) {
|
|
147
|
+
const row = requirePayloadObject(issue, 'jira issue');
|
|
148
|
+
const key = requirePayloadString(row.key, 'issue key', 'jira issue');
|
|
149
|
+
const fields = requirePayloadObject(row.fields, `jira issue ${key} fields`);
|
|
150
|
+
const rendered = row.renderedFields && typeof row.renderedFields === 'object' && !Array.isArray(row.renderedFields)
|
|
151
|
+
? row.renderedFields
|
|
152
|
+
: {};
|
|
153
|
+
const custom = configuredFieldNames();
|
|
154
|
+
const comments = inlineComments(fields, key, options);
|
|
155
|
+
const requireNestedCollections = options.requireNestedCollections !== false;
|
|
156
|
+
const attachments = requireNestedCollections
|
|
157
|
+
? requirePayloadArray(fields.attachment, `jira issue ${key} attachment field`)
|
|
158
|
+
: [];
|
|
159
|
+
const issueLinks = requireNestedCollections
|
|
160
|
+
? requirePayloadArray(fields.issuelinks, `jira issue ${key} issuelinks field`)
|
|
161
|
+
: [];
|
|
162
|
+
const normalized = {
|
|
163
|
+
key,
|
|
164
|
+
id: row.id != null ? String(row.id) : '',
|
|
165
|
+
url: `${config.baseUrl}/browse/${key}`,
|
|
166
|
+
summary: String(fields.summary ?? ''),
|
|
167
|
+
issueType: valueName(fields.issuetype) || undefined,
|
|
168
|
+
status: valueName(fields.status) || undefined,
|
|
169
|
+
priority: valueName(fields.priority) || undefined,
|
|
170
|
+
project: valueName(fields.project) || undefined,
|
|
171
|
+
reporter: displayUser(fields.reporter) || undefined,
|
|
172
|
+
assignee: displayUser(fields.assignee) || undefined,
|
|
173
|
+
labels: Array.isArray(fields.labels) ? fields.labels.map(String) : [],
|
|
174
|
+
description: {
|
|
175
|
+
raw: fields.description ?? null,
|
|
176
|
+
markdown: jiraBodyToMarkdown(fields.description, rendered.description),
|
|
177
|
+
},
|
|
178
|
+
comments: comments.map(normalizeComment),
|
|
179
|
+
attachments: attachments.map(normalizeAttachment),
|
|
180
|
+
linkedIssues: issueLinks.map(normalizeIssueLink),
|
|
181
|
+
fixVersions: valueNames(fields.fixVersions),
|
|
182
|
+
affectedVersions: valueNames(fields.versions),
|
|
183
|
+
components: valueNames(fields.components),
|
|
184
|
+
created: fields.created ? String(fields.created) : undefined,
|
|
185
|
+
updated: fields.updated ? String(fields.updated) : undefined,
|
|
186
|
+
};
|
|
187
|
+
if (custom.acceptanceCriteria && fields[custom.acceptanceCriteria] !== undefined) {
|
|
188
|
+
normalized.acceptanceCriteria = {
|
|
189
|
+
raw: fields[custom.acceptanceCriteria],
|
|
190
|
+
markdown: customValueToMarkdown(fields[custom.acceptanceCriteria]),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (custom.sprint && fields[custom.sprint] !== undefined) {
|
|
194
|
+
normalized.sprint = customValueToMarkdown(fields[custom.sprint]);
|
|
195
|
+
}
|
|
196
|
+
if (custom.storyPoints && fields[custom.storyPoints] !== undefined) {
|
|
197
|
+
normalized.storyPoints = Number(fields[custom.storyPoints]);
|
|
198
|
+
}
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function issueSummaryRow(issue, config) {
|
|
203
|
+
const normalized = normalizeJiraIssue(issue, config, { comments: [], requireNestedCollections: false });
|
|
204
|
+
return {
|
|
205
|
+
key: normalized.key,
|
|
206
|
+
summary: normalized.summary,
|
|
207
|
+
issueType: normalized.issueType,
|
|
208
|
+
status: normalized.status,
|
|
209
|
+
priority: normalized.priority,
|
|
210
|
+
assignee: normalized.assignee,
|
|
211
|
+
updated: normalized.updated,
|
|
212
|
+
url: normalized.url,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function fetchIssue(config, key, extraFields = []) {
|
|
217
|
+
const issue = await jiraRequest(config, `/issue/${encodeURIComponent(key)}`, {
|
|
218
|
+
params: {
|
|
219
|
+
fields: issueFields(extraFields),
|
|
220
|
+
expand: 'renderedFields',
|
|
221
|
+
},
|
|
222
|
+
label: `jira issue ${key}`,
|
|
223
|
+
});
|
|
224
|
+
return requirePayloadObject(issue, `jira issue ${key}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function fetchComments(config, key, limit = 100) {
|
|
228
|
+
const maxResults = parseJiraLimit(limit, 100, 100);
|
|
229
|
+
const data = await jiraRequest(config, `/issue/${encodeURIComponent(key)}/comment`, {
|
|
230
|
+
params: { startAt: 0, maxResults, expand: 'renderedBody' },
|
|
231
|
+
label: `jira comments ${key}`,
|
|
232
|
+
});
|
|
233
|
+
const payload = requirePayloadObject(data, `jira comments ${key}`);
|
|
234
|
+
return requirePayloadArray(payload.comments, `jira comments ${key}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function jiraRowsOrEmpty(rows, label, hint) {
|
|
238
|
+
return requireNonEmptyRows(rows, label, hint);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function jiraConfig() {
|
|
242
|
+
return getJiraConfig();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const __test__ = {
|
|
246
|
+
configuredFieldNames,
|
|
247
|
+
fetchIssue,
|
|
248
|
+
issueSummaryRow,
|
|
249
|
+
jiraApiPath,
|
|
250
|
+
jiraBodyToMarkdown,
|
|
251
|
+
normalizeAttachment,
|
|
252
|
+
normalizeComment,
|
|
253
|
+
normalizeIssueLink,
|
|
254
|
+
normalizeJiraIssue,
|
|
255
|
+
requireIssueKey,
|
|
256
|
+
};
|