@jackwener/opencli 1.7.12 → 1.7.14

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 (419) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12128 -6665
  4. package/clis/1point3acres/digest.js +35 -0
  5. package/clis/1point3acres/forum.js +51 -0
  6. package/clis/1point3acres/forums.js +44 -0
  7. package/clis/1point3acres/hot.js +35 -0
  8. package/clis/1point3acres/latest.js +35 -0
  9. package/clis/1point3acres/notifications.js +64 -0
  10. package/clis/1point3acres/search.js +71 -0
  11. package/clis/1point3acres/thread.js +117 -0
  12. package/clis/1point3acres/user.js +77 -0
  13. package/clis/1point3acres/utils.js +247 -0
  14. package/clis/_shared/desktop-commands.js +4 -0
  15. package/clis/aibase/news.js +110 -0
  16. package/clis/aibase/news.test.js +59 -0
  17. package/clis/amazon/discussion.test.js +1 -28
  18. package/clis/antigravity/watch.js +3 -2
  19. package/clis/arxiv/author.js +44 -0
  20. package/clis/baidu-scholar/search.js +0 -1
  21. package/clis/bbc/topic.js +57 -0
  22. package/clis/bbc/utils.js +79 -0
  23. package/clis/chaoxing/assignments.js +1 -1
  24. package/clis/chaoxing/exams.js +1 -1
  25. package/clis/chatgpt/ask.js +57 -0
  26. package/clis/chatgpt/commands.test.js +45 -0
  27. package/clis/chatgpt/detail.js +46 -0
  28. package/clis/chatgpt/history.js +39 -0
  29. package/clis/chatgpt/image.js +12 -11
  30. package/clis/chatgpt/image.test.js +23 -0
  31. package/clis/chatgpt/new.js +25 -0
  32. package/clis/chatgpt/read.js +43 -0
  33. package/clis/chatgpt/send.js +46 -0
  34. package/clis/chatgpt/status.js +29 -0
  35. package/clis/chatgpt/utils.js +294 -4
  36. package/clis/chatgpt/utils.test.js +13 -0
  37. package/clis/chatgpt-app/ask.js +6 -3
  38. package/clis/chatwise/ask.js +16 -43
  39. package/clis/chatwise/composer.test.js +186 -0
  40. package/clis/chatwise/send.js +2 -24
  41. package/clis/chatwise/utils.js +143 -0
  42. package/clis/claude/ask.js +1 -1
  43. package/clis/claude/detail.js +1 -0
  44. package/clis/claude/history.js +1 -0
  45. package/clis/claude/new.js +1 -0
  46. package/clis/claude/read.js +1 -0
  47. package/clis/claude/send.js +1 -0
  48. package/clis/claude/status.js +1 -0
  49. package/clis/codex/ask.js +15 -9
  50. package/clis/codex/history.js +16 -33
  51. package/clis/codex/projects.js +28 -0
  52. package/clis/codex/read.js +10 -4
  53. package/clis/codex/send.js +10 -3
  54. package/clis/codex/sidebar.js +356 -0
  55. package/clis/codex/sidebar.test.js +329 -0
  56. package/clis/coingecko/categories.js +75 -0
  57. package/clis/coingecko/coin.js +107 -0
  58. package/clis/coingecko/coingecko.test.js +109 -0
  59. package/clis/coingecko/derivatives.js +84 -0
  60. package/clis/coingecko/exchanges.js +74 -0
  61. package/clis/coingecko/global.js +71 -0
  62. package/clis/coingecko/top.js +64 -0
  63. package/clis/coingecko/trending.js +55 -0
  64. package/clis/coupang/add-to-cart.js +21 -13
  65. package/clis/coupang/coupang.test.js +159 -0
  66. package/clis/coupang/product.js +257 -0
  67. package/clis/coupang/search.js +38 -16
  68. package/clis/coupang/utils.js +55 -1
  69. package/clis/crates/crate.js +62 -0
  70. package/clis/crates/search.js +44 -0
  71. package/clis/crates/utils.js +72 -0
  72. package/clis/ctrip/ctrip.test.js +234 -0
  73. package/clis/ctrip/hotel-suggest.js +45 -0
  74. package/clis/ctrip/search.js +22 -68
  75. package/clis/ctrip/utils.js +175 -0
  76. package/clis/cursor/ask.js +6 -3
  77. package/clis/dblp/author.js +133 -0
  78. package/clis/dblp/venue.js +64 -0
  79. package/clis/deepseek/ask.js +12 -7
  80. package/clis/deepseek/ask.test.js +13 -13
  81. package/clis/deepseek/detail.js +38 -0
  82. package/clis/deepseek/detail.test.js +81 -0
  83. package/clis/deepseek/history.js +1 -0
  84. package/clis/deepseek/new.js +1 -0
  85. package/clis/deepseek/read.js +1 -0
  86. package/clis/deepseek/send.js +140 -0
  87. package/clis/deepseek/send.test.js +107 -0
  88. package/clis/deepseek/status.js +1 -0
  89. package/clis/deepseek/utils.js +66 -0
  90. package/clis/deepseek/utils.test.js +107 -1
  91. package/clis/defillama/defillama.test.js +99 -0
  92. package/clis/defillama/protocol.js +84 -0
  93. package/clis/defillama/protocols.js +55 -0
  94. package/clis/defillama/utils.js +99 -0
  95. package/clis/devto/latest.js +74 -0
  96. package/clis/dockerhub/image.js +52 -0
  97. package/clis/dockerhub/search.js +47 -0
  98. package/clis/dockerhub/utils.js +100 -0
  99. package/clis/doubao/ask.js +7 -3
  100. package/clis/doubao/detail.js +1 -0
  101. package/clis/doubao/history.js +1 -0
  102. package/clis/doubao/meeting-summary.js +1 -0
  103. package/clis/doubao/meeting-transcript.js +1 -0
  104. package/clis/doubao/new.js +1 -0
  105. package/clis/doubao/read.js +1 -0
  106. package/clis/doubao/send.js +1 -0
  107. package/clis/doubao/status.js +1 -0
  108. package/clis/douyin/draft.test.js +1 -30
  109. package/clis/endoflife/endoflife.test.js +51 -0
  110. package/clis/endoflife/product.js +55 -0
  111. package/clis/endoflife/utils.js +89 -0
  112. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  113. package/clis/facebook/notifications.js +326 -30
  114. package/clis/facebook/notifications.test.js +458 -0
  115. package/clis/flathub/app.js +71 -0
  116. package/clis/flathub/flathub.test.js +90 -0
  117. package/clis/flathub/search.js +80 -0
  118. package/clis/flathub/utils.js +114 -0
  119. package/clis/gemini/ask.js +7 -3
  120. package/clis/gemini/ask.test.js +2 -2
  121. package/clis/gemini/deep-research-result.js +6 -2
  122. package/clis/gemini/deep-research-result.test.js +15 -14
  123. package/clis/gemini/deep-research.js +8 -4
  124. package/clis/gemini/deep-research.test.js +15 -18
  125. package/clis/gemini/image.js +7 -2
  126. package/clis/gemini/new.js +1 -0
  127. package/clis/gemini/utils.js +0 -4
  128. package/clis/google-scholar/cite.js +0 -1
  129. package/clis/google-scholar/profile.js +0 -1
  130. package/clis/google-scholar/search.js +0 -1
  131. package/clis/goproxy/goproxy.test.js +103 -0
  132. package/clis/goproxy/module.js +47 -0
  133. package/clis/goproxy/utils.js +165 -0
  134. package/clis/goproxy/versions.js +59 -0
  135. package/clis/gov-law/recent.js +0 -1
  136. package/clis/gov-law/search.js +0 -1
  137. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  138. package/clis/gov-policy/__fixtures__/search.html +41 -0
  139. package/clis/gov-policy/gov-policy.test.js +224 -0
  140. package/clis/gov-policy/recent.js +66 -24
  141. package/clis/gov-policy/search.js +65 -23
  142. package/clis/gov-policy/utils.js +54 -0
  143. package/clis/grok/ask.js +49 -265
  144. package/clis/grok/ask.test.js +21 -46
  145. package/clis/grok/detail.js +60 -0
  146. package/clis/grok/history.js +48 -0
  147. package/clis/grok/{image.ts → image.js} +56 -70
  148. package/clis/grok/image.test.ts +20 -0
  149. package/clis/grok/new.js +20 -0
  150. package/clis/grok/read.js +39 -0
  151. package/clis/grok/send.js +50 -0
  152. package/clis/grok/status.js +41 -0
  153. package/clis/grok/utils.js +326 -0
  154. package/clis/grok/utils.test.js +103 -0
  155. package/clis/hf/datasets.js +88 -0
  156. package/clis/hf/hf.test.js +16 -0
  157. package/clis/hf/models.js +91 -0
  158. package/clis/hf/paper.js +79 -0
  159. package/clis/hf/spaces.js +101 -0
  160. package/clis/hf/top.js +1 -0
  161. package/clis/homebrew/cask.js +39 -0
  162. package/clis/homebrew/formula.js +41 -0
  163. package/clis/homebrew/popular.js +54 -0
  164. package/clis/homebrew/utils.js +100 -0
  165. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  166. package/clis/hupu/detail.js +0 -1
  167. package/clis/hupu/hot.js +156 -35
  168. package/clis/hupu/hot.test.js +224 -0
  169. package/clis/hupu/search.js +0 -1
  170. package/clis/instagram/note.js +1 -1
  171. package/clis/instagram/note.test.js +1 -29
  172. package/clis/instagram/post.js +1 -1
  173. package/clis/instagram/post.test.js +1 -1
  174. package/clis/instagram/reel.js +1 -1
  175. package/clis/instagram/story.js +1 -1
  176. package/clis/instagram/story.test.js +1 -34
  177. package/clis/jd/commands.test.js +1 -24
  178. package/clis/lichess/lichess.test.js +85 -0
  179. package/clis/lichess/top.js +46 -0
  180. package/clis/lichess/user.js +91 -0
  181. package/clis/lichess/utils.js +97 -0
  182. package/clis/linkedin/search.js +107 -10
  183. package/clis/linkedin/search.test.js +222 -0
  184. package/clis/linux-do/feed.js +2 -5
  185. package/clis/linux-do/feed.test.js +35 -0
  186. package/clis/lobsters/domain.js +92 -0
  187. package/clis/maven/artifact.js +49 -0
  188. package/clis/maven/search.js +51 -0
  189. package/clis/maven/utils.js +110 -0
  190. package/clis/mdn/search.js +97 -0
  191. package/clis/medium/tag.js +135 -0
  192. package/clis/npm/downloads.js +59 -0
  193. package/clis/npm/package.js +70 -0
  194. package/clis/npm/search.js +49 -0
  195. package/clis/npm/utils.js +76 -0
  196. package/clis/nuget/nuget.test.js +111 -0
  197. package/clis/nuget/package.js +101 -0
  198. package/clis/nuget/search.js +69 -0
  199. package/clis/nuget/utils.js +87 -0
  200. package/clis/nvd/cve.js +121 -0
  201. package/clis/oeis/oeis.test.js +88 -0
  202. package/clis/oeis/search.js +63 -0
  203. package/clis/oeis/sequence.js +71 -0
  204. package/clis/oeis/utils.js +88 -0
  205. package/clis/openalex/search.js +69 -0
  206. package/clis/openalex/utils.js +160 -0
  207. package/clis/openalex/work.js +65 -0
  208. package/clis/openfda/drug-label.js +74 -0
  209. package/clis/openfda/food-recall.js +65 -0
  210. package/clis/openfda/openfda.test.js +114 -0
  211. package/clis/openfda/utils.js +67 -0
  212. package/clis/osv/osv.test.js +97 -0
  213. package/clis/osv/query.js +72 -0
  214. package/clis/osv/utils.js +169 -0
  215. package/clis/osv/vulnerability.js +54 -0
  216. package/clis/packagist/package.js +49 -0
  217. package/clis/packagist/search.js +43 -0
  218. package/clis/packagist/utils.js +113 -0
  219. package/clis/paperreview/feedback.js +1 -1
  220. package/clis/paperreview/review.js +1 -1
  221. package/clis/paperreview/submit.js +1 -1
  222. package/clis/pixiv/download.test.js +1 -1
  223. package/clis/pixiv/illusts.test.js +1 -1
  224. package/clis/pixiv/search.test.js +1 -1
  225. package/clis/pubmed/article.js +50 -0
  226. package/clis/pubmed/author.js +64 -0
  227. package/clis/pubmed/citations.js +36 -0
  228. package/clis/pubmed/pubmed.test.js +276 -0
  229. package/clis/pubmed/related.js +45 -0
  230. package/clis/pubmed/search.js +75 -0
  231. package/clis/pubmed/utils.js +309 -0
  232. package/clis/pypi/downloads.js +66 -0
  233. package/clis/pypi/package.js +79 -0
  234. package/clis/pypi/utils.js +55 -0
  235. package/clis/quark/mv.js +1 -1
  236. package/clis/quark/save.js +1 -1
  237. package/clis/qwen/ask.js +85 -0
  238. package/clis/qwen/detail.js +62 -0
  239. package/clis/qwen/history.js +61 -0
  240. package/clis/qwen/image.js +179 -0
  241. package/clis/qwen/new.js +23 -0
  242. package/clis/qwen/read.js +41 -0
  243. package/clis/qwen/send.js +55 -0
  244. package/clis/qwen/status.js +37 -0
  245. package/clis/qwen/utils.js +409 -0
  246. package/clis/qwen/utils.test.js +45 -0
  247. package/clis/rest-countries/country.js +65 -0
  248. package/clis/rest-countries/region.js +64 -0
  249. package/clis/rest-countries/rest-countries.test.js +83 -0
  250. package/clis/rest-countries/utils.js +126 -0
  251. package/clis/reuters/article-detail.js +53 -0
  252. package/clis/reuters/reuters.test.js +299 -0
  253. package/clis/reuters/search.js +45 -34
  254. package/clis/reuters/utils.js +159 -0
  255. package/clis/rfc/rfc.js +52 -0
  256. package/clis/rfc/rfc.test.js +74 -0
  257. package/clis/rfc/utils.js +72 -0
  258. package/clis/rubygems/gem.js +42 -0
  259. package/clis/rubygems/search.js +47 -0
  260. package/clis/rubygems/utils.js +86 -0
  261. package/clis/stackoverflow/related.js +66 -0
  262. package/clis/stackoverflow/stackoverflow.test.js +58 -0
  263. package/clis/stackoverflow/tag.js +60 -0
  264. package/clis/stackoverflow/user.js +50 -0
  265. package/clis/stackoverflow/utils.js +118 -0
  266. package/clis/steam/app.js +67 -0
  267. package/clis/steam/search.js +58 -0
  268. package/clis/steam/steam.test.js +46 -0
  269. package/clis/steam/utils.js +107 -0
  270. package/clis/taobao/commands.test.js +1 -24
  271. package/clis/test-utils.js +61 -0
  272. package/clis/tieba/hot.js +0 -1
  273. package/clis/tiktok/comment.js +128 -41
  274. package/clis/tiktok/creator-videos.js +270 -0
  275. package/clis/tiktok/creator-videos.test.js +113 -0
  276. package/clis/tiktok/explore.js +137 -29
  277. package/clis/tiktok/follow.js +115 -33
  278. package/clis/tiktok/following.js +157 -36
  279. package/clis/tiktok/friends.js +139 -37
  280. package/clis/tiktok/live.js +137 -41
  281. package/clis/tiktok/notifications.js +141 -38
  282. package/clis/tiktok/refactor.test.js +389 -0
  283. package/clis/tiktok/unfollow.js +124 -38
  284. package/clis/tiktok/user.js +203 -29
  285. package/clis/tiktok/utils.js +505 -0
  286. package/clis/tiktok/write-refactor.test.js +370 -0
  287. package/clis/toutiao/articles.js +36 -62
  288. package/clis/toutiao/hot.js +63 -0
  289. package/clis/toutiao/toutiao.test.js +378 -0
  290. package/clis/toutiao/utils.js +161 -0
  291. package/clis/tvmaze/search.js +61 -0
  292. package/clis/tvmaze/show.js +60 -0
  293. package/clis/tvmaze/tvmaze.test.js +93 -0
  294. package/clis/tvmaze/utils.js +110 -0
  295. package/clis/twitter/accept.js +1 -1
  296. package/clis/twitter/followers.js +134 -69
  297. package/clis/twitter/quote.js +139 -0
  298. package/clis/twitter/quote.test.js +106 -0
  299. package/clis/twitter/reply-dm.js +1 -1
  300. package/clis/twitter/reply.test.js +1 -29
  301. package/clis/twitter/retweet.js +99 -0
  302. package/clis/twitter/retweet.test.js +69 -0
  303. package/clis/twitter/shared.js +38 -0
  304. package/clis/twitter/shared.test.js +28 -1
  305. package/clis/twitter/unlike.js +87 -0
  306. package/clis/twitter/unlike.test.js +72 -0
  307. package/clis/twitter/unretweet.js +99 -0
  308. package/clis/twitter/unretweet.test.js +69 -0
  309. package/clis/uisdc/news.js +105 -0
  310. package/clis/uisdc/news.test.js +66 -0
  311. package/clis/wanfang/search.js +0 -1
  312. package/clis/web/read.js +47 -17
  313. package/clis/web/read.test.js +101 -1
  314. package/clis/weixin/create-draft.js +1 -1
  315. package/clis/weixin/drafts.js +1 -1
  316. package/clis/weixin/drafts.test.js +5 -1
  317. package/clis/weixin/search.js +157 -0
  318. package/clis/weixin/search.test.js +227 -0
  319. package/clis/wikidata/entity.js +60 -0
  320. package/clis/wikidata/search.js +50 -0
  321. package/clis/wikidata/utils.js +117 -0
  322. package/clis/wikidata/wikidata.test.js +83 -0
  323. package/clis/wikipedia/page.js +95 -0
  324. package/clis/wttr/current.js +63 -0
  325. package/clis/wttr/forecast.js +71 -0
  326. package/clis/wttr/utils.js +50 -0
  327. package/clis/wttr/wttr.test.js +84 -0
  328. package/clis/xianyu/chat.js +16 -4
  329. package/clis/xianyu/chat.test.js +64 -0
  330. package/clis/xianyu/publish.js +485 -0
  331. package/clis/xianyu/publish.test.js +220 -0
  332. package/clis/xiaoe/catalog.js +105 -40
  333. package/clis/xiaoe/content.js +164 -29
  334. package/clis/xiaoe/courses.js +86 -29
  335. package/clis/xiaoe/xiaoe.test.js +486 -0
  336. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  337. package/clis/xiaohongshu/publish.js +16 -3
  338. package/clis/xiaohongshu/publish.test.js +46 -1
  339. package/clis/youtube/transcript.js +13 -19
  340. package/clis/youtube/transcript.test.js +17 -0
  341. package/clis/yuanbao/ask.js +17 -66
  342. package/clis/yuanbao/ask.test.js +5 -5
  343. package/clis/yuanbao/detail.js +65 -0
  344. package/clis/yuanbao/history.js +51 -0
  345. package/clis/yuanbao/new.js +1 -0
  346. package/clis/yuanbao/read.js +38 -0
  347. package/clis/yuanbao/send.js +57 -0
  348. package/clis/yuanbao/shared.js +297 -5
  349. package/clis/yuanbao/shared.test.js +80 -0
  350. package/clis/yuanbao/status.js +44 -0
  351. package/clis/zlibrary/commands.test.js +1 -11
  352. package/dist/src/browser/base-page.d.ts +9 -0
  353. package/dist/src/browser/base-page.js +44 -1
  354. package/dist/src/browser/base-page.test.js +66 -0
  355. package/dist/src/browser/bridge.js +47 -45
  356. package/dist/src/browser/cdp.d.ts +1 -0
  357. package/dist/src/browser/cdp.js +51 -9
  358. package/dist/src/browser/daemon-client.d.ts +4 -0
  359. package/dist/src/browser/errors.js +1 -1
  360. package/dist/src/browser/page.d.ts +1 -1
  361. package/dist/src/browser/page.js +3 -1
  362. package/dist/src/browser/page.test.js +29 -0
  363. package/dist/src/browser/target-errors.d.ts +2 -1
  364. package/dist/src/browser/target-errors.js +1 -0
  365. package/dist/src/browser/target-resolver.d.ts +25 -0
  366. package/dist/src/browser/target-resolver.js +43 -0
  367. package/dist/src/browser.test.js +18 -0
  368. package/dist/src/build-manifest.js +9 -4
  369. package/dist/src/build-manifest.test.js +2 -8
  370. package/dist/src/capabilityRouting.d.ts +16 -1
  371. package/dist/src/capabilityRouting.js +24 -1
  372. package/dist/src/capabilityRouting.test.js +19 -1
  373. package/dist/src/cli.js +76 -11
  374. package/dist/src/cli.test.js +241 -1
  375. package/dist/src/commanderAdapter.js +23 -9
  376. package/dist/src/commanderAdapter.test.js +0 -1
  377. package/dist/src/discovery.js +2 -5
  378. package/dist/src/errors.js +1 -1
  379. package/dist/src/execution.d.ts +1 -1
  380. package/dist/src/execution.js +111 -27
  381. package/dist/src/execution.test.js +326 -17
  382. package/dist/src/help.d.ts +27 -2
  383. package/dist/src/help.js +196 -23
  384. package/dist/src/help.test.d.ts +1 -0
  385. package/dist/src/help.test.js +54 -0
  386. package/dist/src/main.js +14 -1
  387. package/dist/src/manifest-types.d.ts +5 -3
  388. package/dist/src/pipeline/executor.js +1 -1
  389. package/dist/src/pipeline/executor.test.js +8 -0
  390. package/dist/src/pipeline/registry.d.ts +9 -0
  391. package/dist/src/pipeline/registry.js +13 -1
  392. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  393. package/dist/src/pipeline/steps/browser.js +10 -0
  394. package/dist/src/pipeline/steps/download.test.js +1 -0
  395. package/dist/src/registry-api.d.ts +1 -1
  396. package/dist/src/registry.d.ts +12 -11
  397. package/dist/src/registry.js +16 -6
  398. package/dist/src/registry.test.js +2 -2
  399. package/dist/src/runtime.d.ts +2 -1
  400. package/dist/src/runtime.js +1 -1
  401. package/dist/src/serialization.d.ts +2 -2
  402. package/dist/src/serialization.js +4 -6
  403. package/dist/src/serialization.test.js +17 -0
  404. package/dist/src/types.d.ts +17 -0
  405. package/dist/src/validate.js +15 -11
  406. package/dist/src/validate.test.d.ts +9 -0
  407. package/dist/src/validate.test.js +90 -0
  408. package/package.json +1 -1
  409. package/scripts/fetch-adapters.js +1 -1
  410. package/scripts/typed-error-lint-baseline.json +5 -77
  411. package/clis/ctrip/search.test.js +0 -64
  412. package/clis/gov-policy/commands.test.js +0 -27
  413. package/clis/linux-do/category.js +0 -37
  414. package/clis/linux-do/hot.js +0 -26
  415. package/clis/linux-do/latest.js +0 -19
  416. package/clis/pixiv/test-utils.js +0 -23
  417. package/clis/toutiao/articles.test.js +0 -30
  418. package/dist/src/analysis.d.ts +0 -40
  419. package/dist/src/analysis.js +0 -172
@@ -0,0 +1,97 @@
1
+ // mdn search — search MDN Web Docs.
2
+ //
3
+ // Hits `https://developer.mozilla.org/api/v1/search?q=…&locale=…`. Returns a
4
+ // row per matched doc with title, slug-derived id, summary preview, and the
5
+ // canonical MDN URL.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
8
+
9
+ const MDN_BASE = 'https://developer.mozilla.org';
10
+ const UA = 'opencli-mdn-adapter (+https://github.com/jackwener/opencli)';
11
+ const ALLOWED_LOCALES = new Set(['en-US', 'de', 'es', 'fr', 'ja', 'ko', 'pt-BR', 'ru', 'zh-CN', 'zh-TW']);
12
+
13
+ function requireString(value, label) {
14
+ const s = String(value ?? '').trim();
15
+ if (!s) throw new ArgumentError(`mdn ${label} cannot be empty`);
16
+ return s;
17
+ }
18
+
19
+ function requireBoundedInt(value, defaultValue, maxValue) {
20
+ const raw = value ?? defaultValue;
21
+ const n = typeof raw === 'number' ? raw : Number(raw);
22
+ if (!Number.isInteger(n) || n <= 0) {
23
+ throw new ArgumentError('mdn limit must be a positive integer');
24
+ }
25
+ if (n > maxValue) {
26
+ throw new ArgumentError(`mdn limit must be <= ${maxValue}`);
27
+ }
28
+ return n;
29
+ }
30
+
31
+ function requireLocale(value) {
32
+ const s = String(value ?? 'en-US').trim();
33
+ if (!ALLOWED_LOCALES.has(s)) {
34
+ throw new ArgumentError(
35
+ `mdn locale "${value}" is not supported`,
36
+ `Allowed locales: ${[...ALLOWED_LOCALES].join(' / ')}`,
37
+ );
38
+ }
39
+ return s;
40
+ }
41
+
42
+ cli({
43
+ site: 'mdn',
44
+ name: 'search',
45
+ access: 'read',
46
+ description: 'Search MDN Web Docs by keyword',
47
+ domain: 'developer.mozilla.org',
48
+ strategy: Strategy.PUBLIC,
49
+ browser: false,
50
+ args: [
51
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "fetch", "flexbox", "Array.prototype.map")' },
52
+ { name: 'limit', type: 'int', default: 10, help: 'Max results (1-50)' },
53
+ { name: 'locale', default: 'en-US', help: 'Doc locale (en-US default; de / es / fr / ja / ko / pt-BR / ru / zh-CN / zh-TW)' },
54
+ ],
55
+ columns: ['rank', 'title', 'slug', 'locale', 'summary', 'url'],
56
+ func: async (args) => {
57
+ const query = requireString(args.query, 'query');
58
+ const limit = requireBoundedInt(args.limit, 10, 50);
59
+ const locale = requireLocale(args.locale);
60
+ const url = `${MDN_BASE}/api/v1/search?q=${encodeURIComponent(query)}&locale=${encodeURIComponent(locale)}&size=${limit}`;
61
+ let resp;
62
+ try {
63
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
64
+ }
65
+ catch (err) {
66
+ throw new CommandExecutionError(`mdn search request failed: ${err?.message ?? err}`);
67
+ }
68
+ if (resp.status === 429) {
69
+ throw new CommandExecutionError(
70
+ 'mdn search returned HTTP 429 (rate limited)',
71
+ 'MDN throttles bursty traffic; wait a few seconds and retry.',
72
+ );
73
+ }
74
+ if (!resp.ok) {
75
+ throw new CommandExecutionError(`mdn search returned HTTP ${resp.status}`);
76
+ }
77
+ let body;
78
+ try {
79
+ body = await resp.json();
80
+ }
81
+ catch (err) {
82
+ throw new CommandExecutionError(`mdn search returned malformed JSON: ${err?.message ?? err}`);
83
+ }
84
+ const docs = Array.isArray(body?.documents) ? body.documents : [];
85
+ if (!docs.length) {
86
+ throw new EmptyResultError('mdn search', `No MDN results matched "${query}" (locale ${locale}).`);
87
+ }
88
+ return docs.slice(0, limit).map((doc, i) => ({
89
+ rank: i + 1,
90
+ title: String(doc.title ?? ''),
91
+ slug: String(doc.slug ?? ''),
92
+ locale: String(doc.locale ?? locale),
93
+ summary: String(doc.summary ?? '').replace(/\s+/g, ' ').trim(),
94
+ url: doc.mdn_url ? `${MDN_BASE}${doc.mdn_url}` : '',
95
+ }));
96
+ },
97
+ });
@@ -0,0 +1,135 @@
1
+ // medium tag — Medium articles for a tag, newest first, via the public
2
+ // RSS feed at `https://medium.com/feed/tag/<tag>`.
3
+ //
4
+ // Complements existing `medium feed` (per-publication / per-user) and
5
+ // `medium search` by surfacing topical streams.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
8
+
9
+ const TAG_PATTERN = /^[a-z0-9][a-z0-9-]*$/i;
10
+
11
+ const HTML_ENTITIES = {
12
+ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'", '&#39;': "'", '&nbsp;': ' ',
13
+ };
14
+
15
+ function decodeHtml(value) {
16
+ return String(value ?? '')
17
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
18
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
19
+ .replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g, (m) => HTML_ENTITIES[m] || m);
20
+ }
21
+
22
+ function extractTag(block, tag) {
23
+ const cdata = block.match(new RegExp(`<${tag}[^>]*>\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*<\\/${tag}>`));
24
+ if (cdata) return cdata[1];
25
+ const plain = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
26
+ return plain ? plain[1] : '';
27
+ }
28
+
29
+ function extractCategories(block) {
30
+ const out = [];
31
+ const re = /<category(?:[^>]*)>(?:<!\[CDATA\[([\s\S]*?)\]\]>|([\s\S]*?))<\/category>/g;
32
+ let m;
33
+ while ((m = re.exec(block)) !== null) {
34
+ const v = decodeHtml((m[1] ?? m[2] ?? '').trim());
35
+ if (v) out.push(v);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ function isoDateFromRfc822(value) {
41
+ if (!value) return '';
42
+ const d = new Date(value);
43
+ if (Number.isNaN(d.getTime())) return '';
44
+ return d.toISOString().slice(0, 10);
45
+ }
46
+
47
+ function stripHtml(value) {
48
+ return decodeHtml(String(value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
49
+ }
50
+
51
+ function requireTag(value) {
52
+ const s = String(value ?? '').trim().toLowerCase();
53
+ if (!s) {
54
+ throw new ArgumentError('medium tag is required (e.g. "programming", "javascript")');
55
+ }
56
+ if (!TAG_PATTERN.test(s)) {
57
+ throw new ArgumentError(
58
+ `medium tag "${value}" is not valid`,
59
+ 'Tags are lowercase alphanumeric, optionally hyphenated (e.g. "machine-learning").',
60
+ );
61
+ }
62
+ return s;
63
+ }
64
+
65
+ function requireBoundedInt(value, defaultValue, maxValue) {
66
+ const raw = value ?? defaultValue;
67
+ const n = typeof raw === 'number' ? raw : Number(raw);
68
+ if (!Number.isInteger(n) || n <= 0) {
69
+ throw new ArgumentError('medium limit must be a positive integer');
70
+ }
71
+ if (n > maxValue) {
72
+ throw new ArgumentError(`medium limit must be <= ${maxValue}`);
73
+ }
74
+ return n;
75
+ }
76
+
77
+ cli({
78
+ site: 'medium',
79
+ name: 'tag',
80
+ access: 'read',
81
+ description: 'Latest Medium articles tagged with a given keyword (RSS feed)',
82
+ domain: 'medium.com',
83
+ strategy: Strategy.PUBLIC,
84
+ browser: false,
85
+ args: [
86
+ { name: 'tag', positional: true, required: true, help: 'Lowercase tag slug (e.g. "programming", "machine-learning")' },
87
+ { name: 'limit', type: 'int', default: 20, help: 'Max articles (1-25 — single RSS page)' },
88
+ ],
89
+ columns: ['rank', 'title', 'author', 'description', 'categories', 'published', 'url'],
90
+ func: async (args) => {
91
+ const tag = requireTag(args.tag);
92
+ const limit = requireBoundedInt(args.limit, 20, 25);
93
+ const url = `https://medium.com/feed/tag/${tag}`;
94
+ let resp;
95
+ try {
96
+ resp = await fetch(url, {
97
+ headers: {
98
+ 'user-agent': 'opencli-medium-adapter (+https://github.com/jackwener/opencli)',
99
+ accept: 'application/rss+xml, application/xml',
100
+ },
101
+ });
102
+ }
103
+ catch (err) {
104
+ throw new CommandExecutionError(
105
+ `medium tag request failed: ${err?.message ?? err}`,
106
+ 'Check that medium.com is reachable from this network.',
107
+ );
108
+ }
109
+ if (resp.status === 404) {
110
+ throw new EmptyResultError('medium tag', `Medium tag "${tag}" does not exist.`);
111
+ }
112
+ if (!resp.ok) {
113
+ throw new CommandExecutionError(`medium tag returned HTTP ${resp.status}`);
114
+ }
115
+ const xml = await resp.text();
116
+ const items = [];
117
+ const re = /<item[^>]*>([\s\S]*?)<\/item>/g;
118
+ let m;
119
+ while ((m = re.exec(xml)) !== null) {
120
+ items.push(m[1]);
121
+ }
122
+ if (!items.length) {
123
+ throw new EmptyResultError('medium tag', `Medium tag "${tag}" RSS feed has no items.`);
124
+ }
125
+ return items.slice(0, limit).map((block, i) => ({
126
+ rank: i + 1,
127
+ title: decodeHtml(extractTag(block, 'title')).trim(),
128
+ author: decodeHtml(extractTag(block, 'dc:creator')).trim(),
129
+ description: stripHtml(extractTag(block, 'description')),
130
+ categories: extractCategories(block).join(', '),
131
+ published: isoDateFromRfc822(decodeHtml(extractTag(block, 'pubDate')).trim()),
132
+ url: decodeHtml(extractTag(block, 'link')).trim(),
133
+ }));
134
+ },
135
+ });
@@ -0,0 +1,59 @@
1
+ // npm downloads — fetch download counts for a single npm package over a window.
2
+ //
3
+ // Hits `api.npmjs.org/downloads/range/<period>/<pkg>`. Default window is the
4
+ // last 7 days (one row per day). Use `--period last-month` for 30 days, or
5
+ // pass a custom `YYYY-MM-DD:YYYY-MM-DD` range.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
8
+ import { NPM_API, npmFetch, requirePackageName } from './utils.js';
9
+
10
+ const FIXED_PERIODS = new Set(['last-day', 'last-week', 'last-month', 'last-year']);
11
+ const RANGE_PATTERN = /^(\d{4}-\d{2}-\d{2}):(\d{4}-\d{2}-\d{2})$/;
12
+
13
+ function requirePeriod(value) {
14
+ const s = String(value ?? 'last-week').trim();
15
+ if (FIXED_PERIODS.has(s)) return s;
16
+ const m = RANGE_PATTERN.exec(s);
17
+ if (m) {
18
+ const [, start, end] = m;
19
+ if (new Date(start) > new Date(end)) {
20
+ throw new ArgumentError(`npm downloads period start ${start} is after end ${end}`);
21
+ }
22
+ return `${start}:${end}`;
23
+ }
24
+ throw new ArgumentError(
25
+ `npm downloads period "${value}" is invalid`,
26
+ 'Use last-day / last-week (default) / last-month / last-year, or YYYY-MM-DD:YYYY-MM-DD.',
27
+ );
28
+ }
29
+
30
+ cli({
31
+ site: 'npm',
32
+ name: 'downloads',
33
+ access: 'read',
34
+ description: 'Daily download counts for an npm package over a window',
35
+ domain: 'api.npmjs.org',
36
+ strategy: Strategy.PUBLIC,
37
+ browser: false,
38
+ args: [
39
+ { name: 'name', positional: true, required: true, help: 'npm package name (e.g. "react", "@vercel/og")' },
40
+ { name: 'period', default: 'last-week', help: 'last-day / last-week / last-month / last-year, or YYYY-MM-DD:YYYY-MM-DD' },
41
+ ],
42
+ columns: ['rank', 'package', 'day', 'downloads'],
43
+ func: async (args) => {
44
+ const name = requirePackageName(args.name);
45
+ const period = requirePeriod(args.period);
46
+ const url = `${NPM_API}/downloads/range/${period}/${name}`;
47
+ const body = await npmFetch(url, `npm downloads ${name}`);
48
+ const days = Array.isArray(body?.downloads) ? body.downloads : [];
49
+ if (!days.length) {
50
+ throw new EmptyResultError('npm downloads', `npm has no download stats for "${name}" in window ${period}.`);
51
+ }
52
+ return days.map((row, i) => ({
53
+ rank: i + 1,
54
+ package: String(body.package ?? name),
55
+ day: String(row.day ?? ''),
56
+ downloads: row.downloads != null ? Number(row.downloads) : null,
57
+ }));
58
+ },
59
+ });
@@ -0,0 +1,70 @@
1
+ // npm package — fetch a single package's registry metadata.
2
+ //
3
+ // Hits `https://registry.npmjs.org/<pkg>` and projects the fields most useful
4
+ // for an agent: name, latest version, description, license, homepage,
5
+ // repository, bug tracker, maintainers, last-modified time. Download stats
6
+ // are intentionally separate (see `npm downloads`) so failure modes don't get
7
+ // silently folded into a registry-metadata response.
8
+ import { cli, Strategy } from '@jackwener/opencli/registry';
9
+ import { EmptyResultError } from '@jackwener/opencli/errors';
10
+ import { NPM_REGISTRY, npmFetch, requirePackageName } from './utils.js';
11
+
12
+ function repoUrl(repo) {
13
+ if (!repo) return '';
14
+ if (typeof repo === 'string') return repo;
15
+ if (typeof repo === 'object' && typeof repo.url === 'string') {
16
+ return repo.url.replace(/^git\+/, '').replace(/\.git$/, '');
17
+ }
18
+ return '';
19
+ }
20
+
21
+ function bugUrl(bugs) {
22
+ if (!bugs) return '';
23
+ if (typeof bugs === 'string') return bugs;
24
+ if (typeof bugs === 'object' && typeof bugs.url === 'string') return bugs.url;
25
+ return '';
26
+ }
27
+
28
+ cli({
29
+ site: 'npm',
30
+ name: 'package',
31
+ access: 'read',
32
+ description: 'Single npm package metadata (latest version, license, homepage, repository). Use `npm downloads` for stats.',
33
+ domain: 'registry.npmjs.org',
34
+ strategy: Strategy.PUBLIC,
35
+ browser: false,
36
+ args: [
37
+ { name: 'name', positional: true, required: true, help: 'npm package name (e.g. "react", "@vercel/og")' },
38
+ ],
39
+ columns: [
40
+ 'name', 'latestVersion', 'description', 'license', 'homepage', 'repository',
41
+ 'bugs', 'maintainers', 'keywords', 'created', 'modified', 'url',
42
+ ],
43
+ func: async (args) => {
44
+ const name = requirePackageName(args.name);
45
+ const body = await npmFetch(`${NPM_REGISTRY}/${name.split('/').map(encodeURIComponent).join('/')}`, `npm package ${name}`);
46
+ const latest = body?.['dist-tags']?.latest;
47
+ if (!latest) {
48
+ throw new EmptyResultError('npm package', `npm registry has no latest version for "${name}".`);
49
+ }
50
+ const v = body?.versions?.[latest] ?? {};
51
+ const maintainers = Array.isArray(body.maintainers)
52
+ ? body.maintainers.map((m) => (typeof m === 'object' && m ? m.name || m.email || '' : String(m))).filter(Boolean).join(', ')
53
+ : '';
54
+ const keywords = Array.isArray(v.keywords) ? v.keywords.join(', ') : '';
55
+ return [{
56
+ name: String(body.name ?? name),
57
+ latestVersion: String(latest),
58
+ description: String(v.description ?? body.description ?? ''),
59
+ license: typeof v.license === 'string' ? v.license : (v.license?.type ?? ''),
60
+ homepage: String(v.homepage ?? ''),
61
+ repository: repoUrl(v.repository),
62
+ bugs: bugUrl(v.bugs),
63
+ maintainers,
64
+ keywords,
65
+ created: String(body.time?.created ?? '').slice(0, 10),
66
+ modified: String(body.time?.modified ?? '').slice(0, 10),
67
+ url: `https://www.npmjs.com/package/${name}`,
68
+ }];
69
+ },
70
+ });
@@ -0,0 +1,49 @@
1
+ // npm search — search the public npm registry by free-text query.
2
+ //
3
+ // Hits `https://registry.npmjs.org/-/v1/search?text=…`. Returns enough per-row
4
+ // to feed back into `npm package` / `npm downloads`: name (round-trips as id),
5
+ // description, version, weekly downloads, dependents count, license, links.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { EmptyResultError } from '@jackwener/opencli/errors';
8
+ import { NPM_REGISTRY, npmFetch, requireBoundedInt, requireString } from './utils.js';
9
+
10
+ cli({
11
+ site: 'npm',
12
+ name: 'search',
13
+ access: 'read',
14
+ description: 'Search the public npm registry by keyword',
15
+ domain: 'registry.npmjs.org',
16
+ strategy: Strategy.PUBLIC,
17
+ browser: false,
18
+ args: [
19
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "react", "graphql client")' },
20
+ { name: 'limit', type: 'int', default: 20, help: 'Max results (1-250)' },
21
+ ],
22
+ columns: ['rank', 'name', 'version', 'description', 'weeklyDownloads', 'dependents', 'license', 'publisher', 'updated', 'url'],
23
+ func: async (args) => {
24
+ const query = requireString(args.query, 'query');
25
+ const limit = requireBoundedInt(args.limit, 20, 250);
26
+ const url = `${NPM_REGISTRY}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`;
27
+ const body = await npmFetch(url, 'npm search');
28
+ const objects = Array.isArray(body?.objects) ? body.objects : [];
29
+ if (!objects.length) {
30
+ throw new EmptyResultError('npm search', `No npm packages matched "${query}".`);
31
+ }
32
+ return objects.slice(0, limit).map((obj, i) => {
33
+ const pkg = obj?.package ?? {};
34
+ const dl = obj?.downloads ?? {};
35
+ return {
36
+ rank: i + 1,
37
+ name: String(pkg.name ?? ''),
38
+ version: String(pkg.version ?? ''),
39
+ description: String(pkg.description ?? ''),
40
+ weeklyDownloads: dl.weekly != null ? Number(dl.weekly) : null,
41
+ dependents: obj.dependents != null ? Number(obj.dependents) : null,
42
+ license: String(pkg.license ?? ''),
43
+ publisher: String(pkg.publisher?.username ?? ''),
44
+ updated: String(obj.updated ?? '').slice(0, 10),
45
+ url: pkg.links?.npm ? String(pkg.links.npm) : (pkg.name ? `https://www.npmjs.com/package/${pkg.name}` : ''),
46
+ };
47
+ });
48
+ },
49
+ });
@@ -0,0 +1,76 @@
1
+ // Shared helpers for the npm adapters that hit the public npm registry
2
+ // (registry.npmjs.org) and download stats API (api.npmjs.org).
3
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
4
+
5
+ export const NPM_REGISTRY = 'https://registry.npmjs.org';
6
+ export const NPM_API = 'https://api.npmjs.org';
7
+ const UA = 'opencli-npm-adapter (+https://github.com/jackwener/opencli)';
8
+
9
+ // npm package names: 1-214 chars, lowercase letters/numbers/-._ , scoped form `@scope/name`.
10
+ const PKG_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
11
+
12
+ export function requireString(value, label) {
13
+ const s = String(value ?? '').trim();
14
+ if (!s) throw new ArgumentError(`npm ${label} cannot be empty`);
15
+ return s;
16
+ }
17
+
18
+ export function requirePackageName(value) {
19
+ const s = String(value ?? '').trim();
20
+ if (!s) throw new ArgumentError('npm package name is required (e.g. "react", "@vercel/og")');
21
+ if (s.length > 214) {
22
+ throw new ArgumentError(`npm package name "${value}" is too long (max 214 chars)`);
23
+ }
24
+ if (!PKG_NAME.test(s)) {
25
+ throw new ArgumentError(
26
+ `npm package name "${value}" is not a valid registry name`,
27
+ 'Names are 1–214 chars of lowercase a-z / 0-9 / "-._" (scoped form: "@scope/name").',
28
+ );
29
+ }
30
+ return s;
31
+ }
32
+
33
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
34
+ const raw = value ?? defaultValue;
35
+ const n = typeof raw === 'number' ? raw : Number(raw);
36
+ if (!Number.isInteger(n) || n <= 0) {
37
+ throw new ArgumentError(`npm ${label} must be a positive integer`);
38
+ }
39
+ if (n > maxValue) {
40
+ throw new ArgumentError(`npm ${label} must be <= ${maxValue}`);
41
+ }
42
+ return n;
43
+ }
44
+
45
+ export async function npmFetch(url, label) {
46
+ let resp;
47
+ try {
48
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
49
+ }
50
+ catch (err) {
51
+ throw new CommandExecutionError(
52
+ `${label} request failed: ${err?.message ?? err}`,
53
+ 'Check that registry.npmjs.org / api.npmjs.org are reachable from this network.',
54
+ );
55
+ }
56
+ if (resp.status === 404) {
57
+ throw new EmptyResultError(label, `npm registry returned 404 for ${url}.`);
58
+ }
59
+ if (resp.status === 429) {
60
+ throw new CommandExecutionError(
61
+ `${label} returned HTTP 429 (rate limited)`,
62
+ 'npm throttles unauthenticated bursts; wait a few seconds and retry.',
63
+ );
64
+ }
65
+ if (!resp.ok) {
66
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
67
+ }
68
+ let body;
69
+ try {
70
+ body = await resp.json();
71
+ }
72
+ catch (err) {
73
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
74
+ }
75
+ return body;
76
+ }
@@ -0,0 +1,111 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
4
+ import './search.js';
5
+ import './package.js';
6
+
7
+ afterEach(() => {
8
+ vi.unstubAllGlobals();
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ describe('nuget search adapter', () => {
13
+ const cmd = getRegistry().get('nuget/search');
14
+
15
+ it('rejects bad args before fetching', async () => {
16
+ const fetchMock = vi.fn();
17
+ vi.stubGlobal('fetch', fetchMock);
18
+ await expect(cmd.func({ query: '' })).rejects.toThrow(ArgumentError);
19
+ await expect(cmd.func({ query: 'foo', limit: 99999 })).rejects.toThrow(ArgumentError);
20
+ expect(fetchMock).not.toHaveBeenCalled();
21
+ });
22
+
23
+ it('maps HTTP 429 to CommandExecutionError', async () => {
24
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('throttled', { status: 429 })));
25
+ await expect(cmd.func({ query: 'newtonsoft' })).rejects.toThrow(CommandExecutionError);
26
+ });
27
+
28
+ it('throws EmptyResultError on empty data list', async () => {
29
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({ data: [] }), { status: 200 })));
30
+ await expect(cmd.func({ query: 'no-such-package' })).rejects.toThrow(EmptyResultError);
31
+ });
32
+
33
+ it('round-trips package id into nuget.org URL', async () => {
34
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
35
+ data: [{
36
+ id: 'Newtonsoft.Json', version: '13.0.3',
37
+ title: 'Json.NET', description: 'Json.NET is a popular high-performance JSON framework for .NET',
38
+ authors: ['James Newton-King'], tags: ['json'], totalDownloads: 1000000, verified: true,
39
+ projectUrl: 'https://www.newtonsoft.com/json',
40
+ }],
41
+ }), { status: 200 })));
42
+ const rows = await cmd.func({ query: 'json', limit: 5 });
43
+ expect(rows[0]).toMatchObject({
44
+ rank: 1, id: 'Newtonsoft.Json', version: '13.0.3', verified: true,
45
+ authors: 'James Newton-King', tags: 'json',
46
+ url: 'https://www.nuget.org/packages/Newtonsoft.Json',
47
+ });
48
+ });
49
+ });
50
+
51
+ describe('nuget package adapter', () => {
52
+ const cmd = getRegistry().get('nuget/package');
53
+
54
+ it('rejects malformed package ids before fetching', async () => {
55
+ const fetchMock = vi.fn();
56
+ vi.stubGlobal('fetch', fetchMock);
57
+ await expect(cmd.func({ id: '' })).rejects.toThrow(ArgumentError);
58
+ await expect(cmd.func({ id: 'has space' })).rejects.toThrow(ArgumentError);
59
+ await expect(cmd.func({ id: '.starts-with-dot' })).rejects.toThrow(ArgumentError);
60
+ expect(fetchMock).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it('maps HTTP 404 to EmptyResultError', async () => {
64
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('missing', { status: 404 })));
65
+ await expect(cmd.func({ id: 'No.Such.Package.1234' })).rejects.toThrow(EmptyResultError);
66
+ });
67
+
68
+ it('flattens inline registration pages and sorts versions newest-first', async () => {
69
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
70
+ items: [
71
+ {
72
+ items: [
73
+ { catalogEntry: { id: 'Newtonsoft.Json', version: '13.0.3', authors: ['JNK'], tags: ['json'], licenseExpression: 'MIT', published: '2023-03-08T20:00:00Z', listed: true } },
74
+ { catalogEntry: { id: 'Newtonsoft.Json', version: '12.0.0', authors: ['JNK'], tags: ['json'], licenseExpression: 'MIT', published: '2018-01-17T20:00:00Z', listed: true } },
75
+ ],
76
+ },
77
+ ],
78
+ }), { status: 200 })));
79
+ const rows = await cmd.func({ id: 'Newtonsoft.Json' });
80
+ expect(rows.map((r) => r.version)).toEqual(['13.0.3', '12.0.0']);
81
+ expect(rows[0]).toMatchObject({
82
+ rank: 1, id: 'Newtonsoft.Json', version: '13.0.3', authors: 'JNK', tags: 'json',
83
+ licenseExpression: 'MIT', listed: true,
84
+ url: 'https://www.nuget.org/packages/Newtonsoft.Json/13.0.3',
85
+ });
86
+ });
87
+
88
+ it('follows registration stub pages instead of silently dropping old versions', async () => {
89
+ const fetchMock = vi.fn()
90
+ .mockResolvedValueOnce(new Response(JSON.stringify({
91
+ items: [{ '@id': 'https://api.nuget.org/v3/registration5-semver1/newtonsoft.json/page/1.json' }],
92
+ }), { status: 200 }))
93
+ .mockResolvedValueOnce(new Response(JSON.stringify({
94
+ items: [
95
+ { catalogEntry: { id: 'Newtonsoft.Json', version: '1.0.0', published: '2010-01-01T00:00:00Z' } },
96
+ { catalogEntry: { id: 'Newtonsoft.Json', version: '2.0.0', published: '2011-01-01T00:00:00Z' } },
97
+ ],
98
+ }), { status: 200 }));
99
+ vi.stubGlobal('fetch', fetchMock);
100
+ const rows = await cmd.func({ id: 'Newtonsoft.Json' });
101
+ expect(fetchMock).toHaveBeenCalledTimes(2);
102
+ expect(rows.map((r) => r.version)).toEqual(['2.0.0', '1.0.0']);
103
+ });
104
+
105
+ it('fails fast on malformed stub pages to avoid partial version history', async () => {
106
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
107
+ items: [{ lower: '1.0.0', upper: '2.0.0' }],
108
+ }), { status: 200 })));
109
+ await expect(cmd.func({ id: 'Newtonsoft.Json' })).rejects.toThrow(CommandExecutionError);
110
+ });
111
+ });