@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.
Files changed (346) hide show
  1. package/README.md +35 -194
  2. package/README.zh-CN.md +42 -260
  3. package/cli-manifest.json +8160 -4392
  4. package/clis/12306/me.js +73 -0
  5. package/clis/12306/orders.js +96 -0
  6. package/clis/12306/passengers.js +90 -0
  7. package/clis/12306/price.js +166 -0
  8. package/clis/12306/stations.js +66 -0
  9. package/clis/12306/train.js +91 -0
  10. package/clis/12306/trains.js +119 -0
  11. package/clis/12306/utils.js +272 -0
  12. package/clis/12306/utils.test.js +331 -0
  13. package/clis/36kr/article.js +6 -3
  14. package/clis/36kr/article.test.js +46 -0
  15. package/clis/_atlassian/shared.js +577 -0
  16. package/clis/_atlassian/shared.test.js +170 -0
  17. package/clis/apple-podcasts/commands.test.js +20 -0
  18. package/clis/apple-podcasts/search.js +2 -2
  19. package/clis/barchart/greeks.js +144 -56
  20. package/clis/barchart/greeks.test.js +138 -0
  21. package/clis/bilibili/comment.js +125 -0
  22. package/clis/bilibili/comment.test.js +153 -0
  23. package/clis/bilibili/comments.js +116 -21
  24. package/clis/bilibili/comments.test.js +77 -18
  25. package/clis/bilibili/subtitle.js +76 -31
  26. package/clis/bilibili/subtitle.test.js +156 -9
  27. package/clis/bilibili/summary.js +167 -0
  28. package/clis/bilibili/summary.test.js +210 -0
  29. package/clis/bilibili/utils.js +63 -5
  30. package/clis/bilibili/utils.test.js +45 -1
  31. package/clis/booking/booking.test.js +356 -0
  32. package/clis/booking/search.js +351 -0
  33. package/clis/chatgpt/envelope.test.js +108 -0
  34. package/clis/chatgpt/image.js +2 -2
  35. package/clis/chatgpt/image.test.js +6 -0
  36. package/clis/chatgpt/utils.js +148 -41
  37. package/clis/chatgpt/utils.test.js +92 -2
  38. package/clis/chess/analyze.js +35 -0
  39. package/clis/chess/analyze.test.js +79 -0
  40. package/clis/chess/game.js +114 -0
  41. package/clis/chess/game.test.js +178 -0
  42. package/clis/chess/games.js +67 -0
  43. package/clis/chess/games.test.js +164 -0
  44. package/clis/chess/stats.js +32 -0
  45. package/clis/chess/stats.test.js +79 -0
  46. package/clis/chess/utils.js +170 -0
  47. package/clis/chess/utils.test.js +230 -0
  48. package/clis/confluence/commands.test.js +195 -0
  49. package/clis/confluence/create.js +39 -0
  50. package/clis/confluence/page.js +23 -0
  51. package/clis/confluence/search.js +34 -0
  52. package/clis/confluence/shared.js +173 -0
  53. package/clis/confluence/update.js +38 -0
  54. package/clis/douyin/_shared/browser-fetch.js +44 -20
  55. package/clis/douyin/_shared/browser-fetch.test.js +22 -1
  56. package/clis/douyin/_shared/evaluate-result.js +16 -0
  57. package/clis/douyin/_shared/tos-upload.js +105 -69
  58. package/clis/douyin/_shared/vod-upload.js +212 -0
  59. package/clis/douyin/_shared/vod-upload.test.js +38 -0
  60. package/clis/douyin/delete.js +137 -4
  61. package/clis/douyin/delete.test.js +90 -1
  62. package/clis/douyin/hashtag.js +84 -23
  63. package/clis/douyin/hashtag.test.js +113 -0
  64. package/clis/douyin/publish-upload-id.test.js +170 -0
  65. package/clis/douyin/publish.js +88 -42
  66. package/clis/douyin/user-videos.js +9 -2
  67. package/clis/douyin/user-videos.test.js +43 -0
  68. package/clis/flomo/memos.js +228 -0
  69. package/clis/flomo/memos.test.js +144 -0
  70. package/clis/geogebra/add-circle.js +46 -0
  71. package/clis/geogebra/add-line.js +35 -0
  72. package/clis/geogebra/add-point.js +27 -0
  73. package/clis/geogebra/add-polygon.js +25 -0
  74. package/clis/geogebra/eval.js +35 -0
  75. package/clis/geogebra/geogebra.test.js +175 -0
  76. package/clis/geogebra/hexagon.js +62 -0
  77. package/clis/geogebra/info.js +72 -0
  78. package/clis/geogebra/list.js +35 -0
  79. package/clis/geogebra/triangle.js +60 -0
  80. package/clis/geogebra/utils.js +271 -0
  81. package/clis/gitee/search.js +2 -2
  82. package/clis/gitee/search.test.js +65 -0
  83. package/clis/jike/post.js +27 -17
  84. package/clis/jike/read.test.js +86 -0
  85. package/clis/jike/topic.js +32 -19
  86. package/clis/jike/user.js +33 -20
  87. package/clis/jira/attachments.js +28 -0
  88. package/clis/jira/commands.test.js +287 -0
  89. package/clis/jira/comments.js +28 -0
  90. package/clis/jira/issue.js +28 -0
  91. package/clis/jira/links.js +28 -0
  92. package/clis/jira/search.js +47 -0
  93. package/clis/jira/shared.js +256 -0
  94. package/clis/lesswrong/comments.js +1 -1
  95. package/clis/lesswrong/curated.js +1 -1
  96. package/clis/lesswrong/frontpage.js +1 -1
  97. package/clis/lesswrong/frontpage.test.js +37 -0
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/top-month.js +1 -1
  104. package/clis/lesswrong/top-week.js +1 -1
  105. package/clis/lesswrong/top-year.js +1 -1
  106. package/clis/lesswrong/top.js +1 -1
  107. package/clis/linkedin/connect.js +401 -0
  108. package/clis/linkedin/connect.test.js +213 -0
  109. package/clis/linkedin/inbox.js +234 -0
  110. package/clis/linkedin/inbox.test.js +152 -0
  111. package/clis/linkedin/job-detail.js +167 -0
  112. package/clis/linkedin/job-detail.test.js +38 -0
  113. package/clis/linkedin/jobs-preferences.js +113 -0
  114. package/clis/linkedin/jobs-preferences.test.js +43 -0
  115. package/clis/linkedin/people-search.js +262 -0
  116. package/clis/linkedin/people-search.test.js +216 -0
  117. package/clis/linkedin/post-analytics.js +74 -0
  118. package/clis/linkedin/post-analytics.test.js +40 -0
  119. package/clis/linkedin/posts-core.js +241 -0
  120. package/clis/linkedin/posts.js +22 -0
  121. package/clis/linkedin/posts.test.js +40 -0
  122. package/clis/linkedin/profile-analytics.js +104 -0
  123. package/clis/linkedin/profile-analytics.test.js +67 -0
  124. package/clis/linkedin/profile-experience.js +671 -0
  125. package/clis/linkedin/profile-experience.test.js +152 -0
  126. package/clis/linkedin/profile-projects.js +311 -0
  127. package/clis/linkedin/profile-projects.test.js +111 -0
  128. package/clis/linkedin/profile-read.js +148 -0
  129. package/clis/linkedin/profile-read.test.js +77 -0
  130. package/clis/linkedin/safe-send.js +357 -0
  131. package/clis/linkedin/safe-send.test.js +204 -0
  132. package/clis/linkedin/salesnav-inbox.js +210 -0
  133. package/clis/linkedin/salesnav-inbox.test.js +113 -0
  134. package/clis/linkedin/salesnav-message.js +360 -0
  135. package/clis/linkedin/salesnav-message.test.js +172 -0
  136. package/clis/linkedin/salesnav-search.js +186 -0
  137. package/clis/linkedin/salesnav-search.test.js +76 -0
  138. package/clis/linkedin/salesnav-thread.js +212 -0
  139. package/clis/linkedin/salesnav-thread.test.js +79 -0
  140. package/clis/linkedin/sent-invitations.js +92 -0
  141. package/clis/linkedin/sent-invitations.test.js +62 -0
  142. package/clis/linkedin/services-read.js +213 -0
  143. package/clis/linkedin/services-read.test.js +105 -0
  144. package/clis/linkedin/shared.js +124 -0
  145. package/clis/linkedin/thread-snapshot.js +214 -0
  146. package/clis/linkedin/thread-snapshot.test.js +89 -0
  147. package/clis/linkedin/timeline.js +14 -7
  148. package/clis/linkedin-learning/course.js +138 -0
  149. package/clis/linkedin-learning/course.test.js +114 -0
  150. package/clis/linkedin-learning/search.js +155 -0
  151. package/clis/linkedin-learning/search.test.js +144 -0
  152. package/clis/linkedin-learning/trending.js +133 -0
  153. package/clis/linkedin-learning/trending.test.js +123 -0
  154. package/clis/notebooklm/add-source.js +269 -0
  155. package/clis/notebooklm/add-source.test.js +97 -0
  156. package/clis/notebooklm/create.js +76 -0
  157. package/clis/notebooklm/create.test.js +58 -0
  158. package/clis/notebooklm/generate-audio.js +91 -0
  159. package/clis/notebooklm/generate-audio.test.js +63 -0
  160. package/clis/notebooklm/generate-slides.js +106 -0
  161. package/clis/notebooklm/generate-slides.test.js +75 -0
  162. package/clis/notebooklm/open.test.js +10 -10
  163. package/clis/notebooklm/rpc.js +20 -6
  164. package/clis/notebooklm/rpc.test.js +27 -1
  165. package/clis/notebooklm/utils.js +100 -24
  166. package/clis/notebooklm/utils.test.js +60 -1
  167. package/clis/notebooklm/write-note.js +103 -0
  168. package/clis/notebooklm/write-note.test.js +70 -0
  169. package/clis/pixiv/detail.js +41 -34
  170. package/clis/pixiv/detail.test.js +93 -0
  171. package/clis/pixiv/user.js +36 -31
  172. package/clis/pixiv/user.test.js +100 -0
  173. package/clis/pixiv/utils.js +56 -7
  174. package/clis/powerchina/search.js +3 -3
  175. package/clis/powerchina/search.test.js +27 -1
  176. package/clis/reddit/extract-media.test.js +149 -0
  177. package/clis/reddit/frontpage.js +47 -9
  178. package/clis/reddit/frontpage.test.js +34 -0
  179. package/clis/reddit/home.js +31 -1
  180. package/clis/reddit/home.test.js +46 -3
  181. package/clis/reddit/hot.js +32 -1
  182. package/clis/reddit/hot.test.js +15 -1
  183. package/clis/reddit/popular.js +39 -1
  184. package/clis/reddit/popular.test.js +26 -0
  185. package/clis/reddit/saved.js +1 -1
  186. package/clis/reddit/search.js +38 -1
  187. package/clis/reddit/search.test.js +26 -0
  188. package/clis/reddit/subreddit.js +52 -7
  189. package/clis/reddit/subreddit.test.js +31 -0
  190. package/clis/reddit/subscribed.js +165 -0
  191. package/clis/reddit/subscribed.test.js +168 -0
  192. package/clis/reddit/upvoted.js +1 -1
  193. package/clis/suno/commands.test.js +188 -0
  194. package/clis/suno/download.js +140 -0
  195. package/clis/suno/download.test.js +151 -0
  196. package/clis/suno/generate.js +231 -0
  197. package/clis/suno/generate.test.js +252 -0
  198. package/clis/suno/list.js +79 -0
  199. package/clis/suno/status.js +63 -0
  200. package/clis/suno/utils.js +549 -0
  201. package/clis/suno/utils.test.js +329 -0
  202. package/clis/twitter/device-follow.js +193 -0
  203. package/clis/twitter/device-follow.test.js +287 -0
  204. package/clis/twitter/download.js +443 -73
  205. package/clis/twitter/download.test.js +457 -0
  206. package/clis/twitter/followers.js +6 -2
  207. package/clis/twitter/followers.test.js +19 -1
  208. package/clis/twitter/following.js +14 -5
  209. package/clis/twitter/following.test.js +29 -0
  210. package/clis/twitter/likes.js +12 -4
  211. package/clis/twitter/likes.test.js +26 -1
  212. package/clis/twitter/list-add.js +1 -1
  213. package/clis/twitter/list-create.js +155 -0
  214. package/clis/twitter/list-create.test.js +169 -0
  215. package/clis/twitter/list-remove.js +13 -6
  216. package/clis/twitter/list-remove.test.js +74 -0
  217. package/clis/twitter/list-tweets.js +6 -2
  218. package/clis/twitter/list-tweets.test.js +41 -1
  219. package/clis/twitter/lists.js +31 -4
  220. package/clis/twitter/lists.test.js +152 -16
  221. package/clis/twitter/notifications.js +4 -4
  222. package/clis/twitter/post.js +62 -4
  223. package/clis/twitter/post.test.js +35 -3
  224. package/clis/twitter/profile.js +81 -28
  225. package/clis/twitter/profile.test.js +113 -2
  226. package/clis/twitter/quote.js +9 -4
  227. package/clis/twitter/reply.js +13 -10
  228. package/clis/twitter/reply.test.js +41 -0
  229. package/clis/twitter/search.js +7 -3
  230. package/clis/twitter/search.test.js +41 -0
  231. package/clis/twitter/shared.js +155 -0
  232. package/clis/twitter/shared.test.js +465 -1
  233. package/clis/twitter/thread.js +10 -2
  234. package/clis/twitter/thread.test.js +58 -0
  235. package/clis/twitter/timeline.js +6 -2
  236. package/clis/twitter/timeline.test.js +2 -0
  237. package/clis/twitter/tweets.js +3 -2
  238. package/clis/twitter/tweets.test.js +1 -1
  239. package/clis/twitter/utils.js +53 -16
  240. package/clis/upwork/detail.js +132 -0
  241. package/clis/upwork/feed.js +109 -0
  242. package/clis/upwork/search.js +115 -0
  243. package/clis/upwork/upwork.test.js +566 -0
  244. package/clis/upwork/utils.js +323 -0
  245. package/clis/weibo/delete.js +172 -0
  246. package/clis/weibo/delete.test.js +94 -0
  247. package/clis/weibo/publish.js +37 -14
  248. package/clis/weibo/publish.test.js +14 -5
  249. package/clis/weibo/user-posts.js +234 -0
  250. package/clis/weibo/user-posts.test.js +92 -0
  251. package/clis/weread/book-search.js +438 -0
  252. package/clis/weread/book-search.test.js +242 -0
  253. package/clis/weread/search-regression.test.js +98 -11
  254. package/clis/weread/search.js +32 -9
  255. package/clis/weread-official/book.js +135 -0
  256. package/clis/weread-official/commands.test.js +385 -0
  257. package/clis/weread-official/discover.js +107 -0
  258. package/clis/weread-official/list-apis.js +95 -0
  259. package/clis/weread-official/notes.js +171 -0
  260. package/clis/weread-official/readdata.js +158 -0
  261. package/clis/weread-official/review.js +93 -0
  262. package/clis/weread-official/search.js +106 -0
  263. package/clis/weread-official/shelf.js +97 -0
  264. package/clis/weread-official/utils.js +293 -0
  265. package/clis/weread-official/utils.test.js +242 -0
  266. package/clis/wikipedia/trending.js +7 -3
  267. package/clis/wikipedia/trending.test.js +57 -0
  268. package/clis/xianyu/chat.js +24 -109
  269. package/clis/xianyu/chat.test.js +5 -0
  270. package/clis/xianyu/im.js +322 -0
  271. package/clis/xianyu/im.test.js +253 -0
  272. package/clis/xianyu/inbox.js +96 -0
  273. package/clis/xianyu/messages.js +91 -0
  274. package/clis/xianyu/reply.js +82 -0
  275. package/clis/xiaohongshu/creator-note-detail.js +166 -28
  276. package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
  277. package/clis/xiaohongshu/creator-notes-summary.js +2 -1
  278. package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
  279. package/clis/xiaohongshu/creator-notes.js +252 -2
  280. package/clis/xiaohongshu/creator-notes.test.js +90 -1
  281. package/clis/xiaohongshu/creator-stats.js +2 -1
  282. package/clis/xiaohongshu/creator-stats.test.js +24 -0
  283. package/clis/xiaohongshu/delete-note.js +260 -0
  284. package/clis/xiaohongshu/delete-note.test.js +172 -0
  285. package/clis/xiaohongshu/download.js +97 -39
  286. package/clis/xiaohongshu/download.test.js +201 -0
  287. package/clis/xiaohongshu/publish.js +48 -8
  288. package/clis/xiaohongshu/publish.test.js +65 -10
  289. package/clis/xiaohongshu/user-helpers.test.js +41 -0
  290. package/clis/xiaohongshu/user.js +27 -4
  291. package/clis/xiaoyuzhou/download.js +1 -1
  292. package/clis/xiaoyuzhou/transcript.js +1 -1
  293. package/clis/youdao/note.js +258 -0
  294. package/clis/youdao/note.test.js +99 -0
  295. package/clis/youtube/transcript.js +397 -24
  296. package/clis/youtube/transcript.test.js +196 -6
  297. package/clis/zhihu/answer-comments.js +280 -0
  298. package/clis/zhihu/answer-comments.test.js +287 -0
  299. package/clis/zhihu/answer-detail.js +2 -19
  300. package/clis/zhihu/answer-detail.test.js +8 -0
  301. package/clis/zhihu/collection.js +17 -16
  302. package/clis/zhihu/collection.test.js +50 -3
  303. package/clis/zhihu/download.js +1 -1
  304. package/clis/zhihu/question.js +42 -17
  305. package/clis/zhihu/question.test.js +113 -11
  306. package/clis/zhihu/search.js +195 -43
  307. package/clis/zhihu/search.test.js +198 -0
  308. package/clis/zhihu/text.js +29 -0
  309. package/clis/zhihu/text.test.js +24 -0
  310. package/dist/src/browser/errors.js +4 -2
  311. package/dist/src/browser/errors.test.js +6 -0
  312. package/dist/src/browser/network-cache.js +13 -1
  313. package/dist/src/browser/network-cache.test.js +17 -0
  314. package/dist/src/browser/page.js +30 -4
  315. package/dist/src/browser/page.test.js +42 -0
  316. package/dist/src/browser/utils.d.ts +1 -1
  317. package/dist/src/cli-argv-preprocess.d.ts +26 -0
  318. package/dist/src/cli-argv-preprocess.js +138 -0
  319. package/dist/src/cli-argv-preprocess.test.js +79 -0
  320. package/dist/src/convention-audit.js +15 -8
  321. package/dist/src/convention-audit.test.js +21 -0
  322. package/dist/src/download/index.js +13 -1
  323. package/dist/src/download/index.test.js +23 -1
  324. package/dist/src/download/media-download.js +15 -2
  325. package/dist/src/download/media-download.test.d.ts +1 -0
  326. package/dist/src/download/media-download.test.js +112 -0
  327. package/dist/src/download/progress.js +2 -2
  328. package/dist/src/download/progress.test.js +12 -1
  329. package/dist/src/electron-apps.js +1 -1
  330. package/dist/src/electron-apps.test.js +7 -2
  331. package/dist/src/errors.d.ts +17 -0
  332. package/dist/src/errors.js +22 -0
  333. package/dist/src/external-clis.yaml +8 -0
  334. package/dist/src/main.js +14 -2
  335. package/dist/src/output.js +11 -1
  336. package/dist/src/output.test.js +6 -0
  337. package/dist/src/registry.js +1 -0
  338. package/dist/src/registry.test.js +11 -0
  339. package/dist/src/utils.d.ts +43 -0
  340. package/dist/src/utils.js +97 -0
  341. package/dist/src/utils.test.d.ts +1 -0
  342. package/dist/src/utils.test.js +155 -0
  343. package/package.json +8 -2
  344. package/scripts/silent-column-drop-baseline.json +0 -52
  345. package/scripts/typed-error-lint-baseline.json +28 -380
  346. package/clis/slock/_utils.js +0 -12
@@ -0,0 +1,577 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { htmlToMarkdown as coreHtmlToMarkdown } from '@jackwener/opencli/utils';
3
+ import {
4
+ ArgumentError,
5
+ AuthRequiredError,
6
+ CommandExecutionError,
7
+ ConfigError,
8
+ EmptyResultError,
9
+ } from '@jackwener/opencli/errors';
10
+
11
+ const USER_AGENT = 'opencli-atlassian-adapter (+https://github.com/jackwener/opencli)';
12
+ const DEPLOYMENTS = new Set(['cloud', 'datacenter', 'auto']);
13
+
14
+ function firstEnv(names) {
15
+ for (const name of names) {
16
+ const value = process.env[name]?.trim();
17
+ if (value) return value;
18
+ }
19
+ return '';
20
+ }
21
+
22
+ function normalizeBaseUrl(value, label) {
23
+ const raw = String(value ?? '').trim();
24
+ if (!raw) {
25
+ throw new ConfigError(`Missing ${label}`, `Set ${label}, for example https://example.atlassian.net`);
26
+ }
27
+ let parsed;
28
+ try {
29
+ parsed = new URL(raw);
30
+ } catch {
31
+ throw new ConfigError(`Invalid ${label}: ${raw}`, 'Use an absolute http(s) URL.');
32
+ }
33
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
34
+ throw new ConfigError(`Invalid ${label}: ${raw}`, 'Use an http(s) URL.');
35
+ }
36
+ parsed.hash = '';
37
+ parsed.search = '';
38
+ return parsed.toString().replace(/\/+$/, '');
39
+ }
40
+
41
+ function parseDeployment(raw, baseUrl) {
42
+ const value = String(raw || 'auto').trim().toLowerCase();
43
+ if (!DEPLOYMENTS.has(value)) {
44
+ throw new ConfigError('Invalid ATLASSIAN_DEPLOYMENT', 'Expected one of: cloud, datacenter, auto.');
45
+ }
46
+ if (value !== 'auto') return value;
47
+ const host = new URL(baseUrl).hostname;
48
+ return host === 'atlassian.net' || host.endsWith('.atlassian.net') ? 'cloud' : 'datacenter';
49
+ }
50
+
51
+ function appendPath(baseUrl, suffix) {
52
+ const base = new URL(baseUrl);
53
+ const path = base.pathname.replace(/\/+$/, '');
54
+ base.pathname = `${path}${suffix}`;
55
+ return base.toString().replace(/\/+$/, '');
56
+ }
57
+
58
+ function normalizeConfluenceBaseUrl(baseUrl, deployment) {
59
+ if (deployment !== 'cloud') return baseUrl;
60
+ const parsed = new URL(baseUrl);
61
+ const normalized = parsed.pathname.replace(/\/+$/, '');
62
+ if (normalized === '/wiki' || normalized.endsWith('/wiki')) return baseUrl;
63
+ return appendPath(baseUrl, '/wiki');
64
+ }
65
+
66
+ function basicAuth(user, token) {
67
+ return `Basic ${Buffer.from(`${user}:${token}`, 'utf8').toString('base64')}`;
68
+ }
69
+
70
+ function resolveAuthHeaders(deployment, productLabel) {
71
+ const bearer = firstEnv(['ATLASSIAN_BEARER_TOKEN', 'ATLASSIAN_OAUTH_TOKEN']);
72
+ if (bearer) return { Authorization: `Bearer ${bearer}` };
73
+
74
+ const pat = firstEnv(['ATLASSIAN_PAT', `${productLabel.toUpperCase()}_PAT`]);
75
+ if (deployment === 'datacenter' && pat) return { Authorization: `Bearer ${pat}` };
76
+
77
+ const prefix = productLabel.toUpperCase();
78
+ const email = firstEnv(['ATLASSIAN_EMAIL', 'ATLASSIAN_USERNAME', `${prefix}_EMAIL`, `${prefix}_USERNAME`]);
79
+ const token = firstEnv(['ATLASSIAN_API_TOKEN', 'ATLASSIAN_PASSWORD', `${prefix}_API_TOKEN`, `${prefix}_PASSWORD`]);
80
+ if (email && token) return { Authorization: basicAuth(email, token) };
81
+
82
+ if (deployment === 'cloud') {
83
+ throw new ConfigError(
84
+ 'Missing Atlassian Cloud credentials',
85
+ 'Set ATLASSIAN_EMAIL and ATLASSIAN_API_TOKEN, or set ATLASSIAN_BEARER_TOKEN for OAuth.',
86
+ );
87
+ }
88
+ throw new ConfigError(
89
+ 'Missing Atlassian Data Center credentials',
90
+ 'Set ATLASSIAN_PAT, ATLASSIAN_BEARER_TOKEN, or ATLASSIAN_USERNAME plus ATLASSIAN_PASSWORD.',
91
+ );
92
+ }
93
+
94
+ export function getJiraConfig() {
95
+ const baseUrl = normalizeBaseUrl(firstEnv(['ATLASSIAN_JIRA_BASE_URL', 'JIRA_BASE_URL']), 'ATLASSIAN_JIRA_BASE_URL');
96
+ const deployment = parseDeployment(process.env.ATLASSIAN_DEPLOYMENT, baseUrl);
97
+ return {
98
+ product: 'jira',
99
+ baseUrl,
100
+ deployment,
101
+ authHeaders: resolveAuthHeaders(deployment, 'jira'),
102
+ };
103
+ }
104
+
105
+ export function getConfluenceConfig() {
106
+ const initialBaseUrl = normalizeBaseUrl(
107
+ firstEnv(['ATLASSIAN_CONFLUENCE_BASE_URL', 'CONFLUENCE_BASE_URL']),
108
+ 'ATLASSIAN_CONFLUENCE_BASE_URL',
109
+ );
110
+ const deployment = parseDeployment(process.env.ATLASSIAN_DEPLOYMENT, initialBaseUrl);
111
+ return {
112
+ product: 'confluence',
113
+ baseUrl: normalizeConfluenceBaseUrl(initialBaseUrl, deployment),
114
+ deployment,
115
+ authHeaders: resolveAuthHeaders(deployment, 'confluence'),
116
+ };
117
+ }
118
+
119
+ function joinUrl(baseUrl, apiPath) {
120
+ if (/^https?:\/\//i.test(apiPath)) return apiPath;
121
+ const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
122
+ return `${baseUrl}${path}`;
123
+ }
124
+
125
+ function summarizeApiError(parsed, fallback) {
126
+ if (parsed && typeof parsed === 'object') {
127
+ const messages = [];
128
+ if (Array.isArray(parsed.errorMessages)) messages.push(...parsed.errorMessages.filter(Boolean));
129
+ if (typeof parsed.message === 'string') messages.push(parsed.message);
130
+ if (typeof parsed.error === 'string') messages.push(parsed.error);
131
+ if (typeof parsed.reason === 'string') messages.push(parsed.reason);
132
+ if (parsed.errors && typeof parsed.errors === 'object') {
133
+ for (const [key, value] of Object.entries(parsed.errors)) {
134
+ messages.push(`${key}: ${String(value)}`);
135
+ }
136
+ }
137
+ if (messages.length) return messages.join(' · ');
138
+ }
139
+ if (typeof parsed === 'string' && parsed.trim()) return parsed.trim().slice(0, 300);
140
+ return fallback;
141
+ }
142
+
143
+ async function parseResponseBody(resp, label) {
144
+ let text;
145
+ try {
146
+ text = await resp.text();
147
+ } catch (err) {
148
+ throw new CommandExecutionError(
149
+ `${label} response body could not be read: ${err?.message ?? err}`,
150
+ 'Check whether the Atlassian instance, proxy, or network interrupted the response.',
151
+ );
152
+ }
153
+ if (!text) return null;
154
+ try {
155
+ return JSON.parse(text);
156
+ } catch {
157
+ return text;
158
+ }
159
+ }
160
+
161
+ export async function atlassianRequest(config, apiPath, options = {}) {
162
+ const method = (options.method ?? 'GET').toUpperCase();
163
+ const label = options.label ?? `${config.product} ${method} ${apiPath}`;
164
+ const headers = {
165
+ 'user-agent': USER_AGENT,
166
+ accept: 'application/json',
167
+ ...config.authHeaders,
168
+ ...(options.headers ?? {}),
169
+ };
170
+ let body;
171
+ if (options.body !== undefined) {
172
+ headers['content-type'] = headers['content-type'] ?? 'application/json';
173
+ body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
174
+ }
175
+
176
+ let resp;
177
+ const url = joinUrl(config.baseUrl, apiPath);
178
+ try {
179
+ resp = await fetch(url, { method, headers, body });
180
+ } catch (err) {
181
+ throw new CommandExecutionError(
182
+ `${label} request failed: ${err?.message ?? err}`,
183
+ 'Check the Atlassian base URL, VPN/network access, and proxy settings.',
184
+ );
185
+ }
186
+
187
+ const parsed = await parseResponseBody(resp, label);
188
+ if (resp.status === 401) {
189
+ throw new AuthRequiredError(
190
+ config.baseUrl,
191
+ `${label} returned HTTP 401`,
192
+ 'Check Atlassian credentials and whether this instance accepts the configured auth method.',
193
+ );
194
+ }
195
+ if (resp.status === 403) {
196
+ throw new AuthRequiredError(
197
+ config.baseUrl,
198
+ `${label} returned HTTP 403: ${summarizeApiError(parsed, 'forbidden')}`,
199
+ 'The authenticated user lacks permission for this Jira issue, Confluence page, or space.',
200
+ );
201
+ }
202
+ if (resp.status === 404) {
203
+ throw new EmptyResultError(label, `Atlassian returned 404 for ${url}.`);
204
+ }
205
+ if (resp.status === 409) {
206
+ throw new CommandExecutionError(
207
+ `${label} returned HTTP 409: ${summarizeApiError(parsed, 'version conflict')}`,
208
+ 'Reload the current Confluence page version and retry the update.',
209
+ );
210
+ }
211
+ if (resp.status === 429) {
212
+ throw new CommandExecutionError(`${label} returned HTTP 429 (rate limited)`, 'Wait and retry with a smaller limit.');
213
+ }
214
+ if (!resp.ok) {
215
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}: ${summarizeApiError(parsed, resp.statusText)}`);
216
+ }
217
+ if (typeof parsed === 'string') {
218
+ throw new CommandExecutionError(
219
+ `${label} returned a non-JSON response`,
220
+ 'Expected Atlassian REST API JSON. Check the base URL and whether an HTML login, SSO, or proxy page was returned.',
221
+ );
222
+ }
223
+ return parsed;
224
+ }
225
+
226
+ export function queryString(params) {
227
+ const qs = new URLSearchParams();
228
+ for (const [key, value] of Object.entries(params)) {
229
+ if (value === undefined || value === null || value === '') continue;
230
+ if (Array.isArray(value)) {
231
+ for (const item of value) qs.append(key, String(item));
232
+ } else {
233
+ qs.set(key, String(value));
234
+ }
235
+ }
236
+ const s = qs.toString();
237
+ return s ? `?${s}` : '';
238
+ }
239
+
240
+ export function requireString(value, label) {
241
+ const s = String(value ?? '').trim();
242
+ if (!s) throw new ArgumentError(`${label} is required`);
243
+ return s;
244
+ }
245
+
246
+ export function requirePayloadObject(value, label) {
247
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
248
+ throw new CommandExecutionError(`${label} returned an unexpected payload shape; expected an object.`);
249
+ }
250
+ return value;
251
+ }
252
+
253
+ export function requirePayloadArray(value, label) {
254
+ if (!Array.isArray(value)) {
255
+ throw new CommandExecutionError(`${label} returned an unexpected payload shape; expected an array.`);
256
+ }
257
+ return value;
258
+ }
259
+
260
+ export function requirePayloadString(value, field, label) {
261
+ if (typeof value !== 'string' && typeof value !== 'number') {
262
+ throw new CommandExecutionError(`${label} did not include a stable ${field}.`);
263
+ }
264
+ const s = String(value).trim();
265
+ if (!s) throw new CommandExecutionError(`${label} did not include a stable ${field}.`);
266
+ return s;
267
+ }
268
+
269
+ export function requireNonEmptyRows(rows, label, hint) {
270
+ if (!rows.length) throw new EmptyResultError(label, hint);
271
+ return rows;
272
+ }
273
+
274
+ export function parseLimit(value, defaultValue = 20, maxValue = 100, label = 'limit') {
275
+ const raw = value ?? defaultValue;
276
+ const n = typeof raw === 'number' ? raw : Number(raw);
277
+ if (!Number.isInteger(n) || n <= 0) {
278
+ throw new ArgumentError(`${label} must be a positive integer`);
279
+ }
280
+ if (n > maxValue) {
281
+ throw new ArgumentError(`${label} must be <= ${maxValue}`);
282
+ }
283
+ return n;
284
+ }
285
+
286
+ export function requireExecute(args, commandName) {
287
+ if (args.execute !== true) {
288
+ throw new ArgumentError(`${commandName} requires --execute to perform a remote write`);
289
+ }
290
+ }
291
+
292
+ export async function readUtf8File(filePath) {
293
+ const path = requireString(filePath, '--file');
294
+ let fileStat;
295
+ try {
296
+ fileStat = await stat(path);
297
+ } catch {
298
+ throw new ArgumentError(`File not found: ${path}`);
299
+ }
300
+ if (!fileStat.isFile()) {
301
+ throw new ArgumentError(`File must be a readable text file: ${path}`);
302
+ }
303
+ let raw;
304
+ try {
305
+ raw = await readFile(path);
306
+ } catch {
307
+ throw new ArgumentError(`File could not be read: ${path}`);
308
+ }
309
+ try {
310
+ return new TextDecoder('utf-8', { fatal: true }).decode(raw);
311
+ } catch {
312
+ throw new ArgumentError(`File could not be decoded as UTF-8 text: ${path}`);
313
+ }
314
+ }
315
+
316
+ export function htmlEscape(value) {
317
+ return String(value ?? '')
318
+ .replace(/&/g, '&amp;')
319
+ .replace(/</g, '&lt;')
320
+ .replace(/>/g, '&gt;')
321
+ .replace(/"/g, '&quot;');
322
+ }
323
+
324
+ export function htmlToMarkdown(html) {
325
+ return coreHtmlToMarkdown(String(html ?? ''));
326
+ }
327
+
328
+ function applyAdfMarks(text, marks = []) {
329
+ let out = text;
330
+ for (const mark of marks) {
331
+ const type = mark?.type;
332
+ if (type === 'link' && mark.attrs?.href) out = `[${out}](${mark.attrs.href})`;
333
+ else if (type === 'strong') out = `**${out}**`;
334
+ else if (type === 'em') out = `_${out}_`;
335
+ else if (type === 'code') out = `\`${out}\``;
336
+ else if (type === 'strike') out = `~~${out}~~`;
337
+ }
338
+ return out;
339
+ }
340
+
341
+ function renderAdfNode(node, depth = 0) {
342
+ if (!node || typeof node !== 'object') return '';
343
+ const content = Array.isArray(node.content) ? node.content : [];
344
+ const renderChildren = (sep = '') => content.map((child) => renderAdfNode(child, depth)).filter(Boolean).join(sep);
345
+ switch (node.type) {
346
+ case 'doc':
347
+ return content.map((child) => renderAdfNode(child, depth)).filter(Boolean).join('\n\n').trim();
348
+ case 'paragraph':
349
+ return renderChildren('');
350
+ case 'text':
351
+ return applyAdfMarks(String(node.text ?? ''), Array.isArray(node.marks) ? node.marks : []);
352
+ case 'hardBreak':
353
+ return '\n';
354
+ case 'heading':
355
+ return `${'#'.repeat(Math.max(1, Math.min(6, Number(node.attrs?.level ?? 2))))} ${renderChildren('')}`;
356
+ case 'bulletList':
357
+ return content.map((child) => renderAdfListItem(child, depth, '-')).join('\n');
358
+ case 'orderedList':
359
+ return content.map((child, i) => renderAdfListItem(child, depth, `${i + 1}.`)).join('\n');
360
+ case 'listItem':
361
+ return renderChildren('\n');
362
+ case 'codeBlock':
363
+ return `\`\`\`\n${renderChildren('')}\n\`\`\``;
364
+ case 'blockquote':
365
+ return renderChildren('\n').split('\n').map((line) => `> ${line}`).join('\n');
366
+ case 'rule':
367
+ return '---';
368
+ case 'table':
369
+ return renderAdfTable(content);
370
+ case 'tableRow':
371
+ return content.map((cell) => escapeMarkdownTableCell(renderAdfNode(cell, depth))).join(' | ');
372
+ case 'tableHeader':
373
+ case 'tableCell':
374
+ return renderChildren(' ').replace(/\s+/g, ' ').trim();
375
+ case 'mention':
376
+ return node.attrs?.text ? String(node.attrs.text) : '';
377
+ case 'emoji':
378
+ return String(node.attrs?.shortName ?? node.attrs?.text ?? '');
379
+ case 'inlineCard':
380
+ return node.attrs?.url ? String(node.attrs.url) : '';
381
+ default:
382
+ return renderChildren('');
383
+ }
384
+ }
385
+
386
+ function renderAdfListItem(node, depth, marker) {
387
+ const indent = ' '.repeat(depth);
388
+ const body = renderAdfNode(node, depth + 1).trim();
389
+ const lines = body.split('\n');
390
+ const [first, ...rest] = lines;
391
+ return `${indent}${marker} ${first ?? ''}${rest.length ? `\n${rest.map((line) => `${indent} ${line}`).join('\n')}` : ''}`;
392
+ }
393
+
394
+ function escapeMarkdownTableCell(value) {
395
+ return String(value ?? '').replace(/\|/g, '\\|').replace(/\n+/g, '<br>').trim();
396
+ }
397
+
398
+ function renderAdfTable(rows) {
399
+ const matrix = rows
400
+ .map((row) => {
401
+ const cells = Array.isArray(row?.content) ? row.content : [];
402
+ return cells.map((cell) => escapeMarkdownTableCell(renderAdfNode(cell)));
403
+ })
404
+ .filter((row) => row.length > 0);
405
+ if (!matrix.length) return '';
406
+ const colCount = Math.max(...matrix.map((row) => row.length));
407
+ const normalize = (row) => Array.from({ length: colCount }, (_value, index) => row[index] ?? '').join(' | ');
408
+ return [
409
+ normalize(matrix[0]),
410
+ Array.from({ length: colCount }, () => '---').join(' | '),
411
+ ...matrix.slice(1).map(normalize),
412
+ ].join('\n');
413
+ }
414
+
415
+ export function adfToMarkdown(value) {
416
+ if (!value) return '';
417
+ if (typeof value === 'string') return value.trim();
418
+ return renderAdfNode(value).trim();
419
+ }
420
+
421
+ function renderInlineMarkdown(value) {
422
+ const src = String(value ?? '');
423
+ const linkRe = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g;
424
+ let out = '';
425
+ let last = 0;
426
+ for (const match of src.matchAll(linkRe)) {
427
+ out += htmlEscape(src.slice(last, match.index));
428
+ out += `<a href="${htmlEscape(match[2])}">${htmlEscape(match[1])}</a>`;
429
+ last = match.index + match[0].length;
430
+ }
431
+ out += htmlEscape(src.slice(last));
432
+ return out
433
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
434
+ .replace(/`([^`]+)`/g, '<code>$1</code>');
435
+ }
436
+
437
+ function isMarkdownTable(lines, index) {
438
+ return lines[index]?.includes('|') && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1] ?? '');
439
+ }
440
+
441
+ function parseTableRow(line) {
442
+ return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim());
443
+ }
444
+
445
+ function renderMarkdownTable(lines, start) {
446
+ const rows = [];
447
+ let index = start;
448
+ rows.push(parseTableRow(lines[index]));
449
+ index += 2;
450
+ while (index < lines.length && lines[index].includes('|') && lines[index].trim()) {
451
+ rows.push(parseTableRow(lines[index]));
452
+ index += 1;
453
+ }
454
+ const htmlRows = rows.map((row, rowIndex) => {
455
+ const tag = rowIndex === 0 ? 'th' : 'td';
456
+ return `<tr>${row.map((cell) => `<${tag}>${renderInlineMarkdown(cell)}</${tag}>`).join('')}</tr>`;
457
+ }).join('');
458
+ return { html: `<table><tbody>${htmlRows}</tbody></table>`, next: index };
459
+ }
460
+
461
+ export function markdownToConfluenceStorage(markdown) {
462
+ const lines = String(markdown ?? '').replace(/\r\n/g, '\n').split('\n');
463
+ const out = [];
464
+ let i = 0;
465
+ let inCode = false;
466
+ let codeLines = [];
467
+ const listStack = [];
468
+
469
+ const closeOneList = () => {
470
+ const current = listStack.pop();
471
+ if (!current) return;
472
+ if (current.liOpen) out.push('</li>');
473
+ out.push(`</${current.tag}>`);
474
+ };
475
+
476
+ const closeListsTo = (indent) => {
477
+ while (listStack.length && listStack[listStack.length - 1].indent > indent) closeOneList();
478
+ };
479
+
480
+ const closeAllLists = () => {
481
+ while (listStack.length) closeOneList();
482
+ };
483
+
484
+ const openList = (tag, indent) => {
485
+ out.push(`<${tag}>`);
486
+ listStack.push({ tag, indent, liOpen: false });
487
+ };
488
+
489
+ const renderListItem = (tag, indent, text) => {
490
+ closeListsTo(indent);
491
+ let current = listStack[listStack.length - 1];
492
+ if (current && current.indent === indent && current.tag !== tag) {
493
+ closeOneList();
494
+ current = listStack[listStack.length - 1];
495
+ }
496
+ if (!current || current.indent < indent) {
497
+ openList(tag, indent);
498
+ current = listStack[listStack.length - 1];
499
+ }
500
+ if (current.indent === indent && current.liOpen) {
501
+ out.push('</li>');
502
+ current.liOpen = false;
503
+ }
504
+ out.push(`<li>${renderInlineMarkdown(text)}`);
505
+ current.liOpen = true;
506
+ };
507
+
508
+ while (i < lines.length) {
509
+ const line = lines[i];
510
+ const fence = line.match(/^```/);
511
+ if (fence) {
512
+ if (inCode) {
513
+ out.push(`<ac:structured-macro ac:name="code"><ac:plain-text-body><![CDATA[${codeLines.join('\n')}]]></ac:plain-text-body></ac:structured-macro>`);
514
+ codeLines = [];
515
+ inCode = false;
516
+ } else {
517
+ closeAllLists();
518
+ inCode = true;
519
+ }
520
+ i += 1;
521
+ continue;
522
+ }
523
+ if (inCode) {
524
+ codeLines.push(line);
525
+ i += 1;
526
+ continue;
527
+ }
528
+ if (!line.trim()) {
529
+ closeAllLists();
530
+ i += 1;
531
+ continue;
532
+ }
533
+ if (isMarkdownTable(lines, i)) {
534
+ closeAllLists();
535
+ const table = renderMarkdownTable(lines, i);
536
+ out.push(table.html);
537
+ i = table.next;
538
+ continue;
539
+ }
540
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
541
+ if (heading) {
542
+ closeAllLists();
543
+ out.push(`<h${heading[1].length}>${renderInlineMarkdown(heading[2])}</h${heading[1].length}>`);
544
+ i += 1;
545
+ continue;
546
+ }
547
+ const unordered = line.match(/^(\s*)[-*]\s+(.+)$/);
548
+ const ordered = line.match(/^(\s*)\d+\.\s+(.+)$/);
549
+ if (unordered || ordered) {
550
+ const match = unordered || ordered;
551
+ const indent = match[1].replace(/\t/g, ' ').length;
552
+ renderListItem(unordered ? 'ul' : 'ol', indent, match[2]);
553
+ i += 1;
554
+ continue;
555
+ }
556
+ closeAllLists();
557
+ out.push(`<p>${renderInlineMarkdown(line)}</p>`);
558
+ i += 1;
559
+ }
560
+
561
+ closeAllLists();
562
+ if (inCode) {
563
+ out.push(`<ac:structured-macro ac:name="code"><ac:plain-text-body><![CDATA[${codeLines.join('\n')}]]></ac:plain-text-body></ac:structured-macro>`);
564
+ }
565
+ return out.join('\n');
566
+ }
567
+
568
+ export const __test__ = {
569
+ adfToMarkdown,
570
+ atlassianRequest,
571
+ getConfluenceConfig,
572
+ getJiraConfig,
573
+ htmlToMarkdown,
574
+ markdownToConfluenceStorage,
575
+ parseLimit,
576
+ queryString,
577
+ };