@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,67 @@
1
+ // openFDA shared helpers — FDA drug labels + food recall enforcement (no auth, public).
2
+ //
3
+ // Free public tier with anonymous rate limit (~240 req/min, 1000 req/day per IP).
4
+ // API key bumps that to 240 req/min × ~120000 req/day, but is not required for
5
+ // modest read traffic.
6
+ import { ArgumentError, EmptyResultError, CommandExecutionError } from '@jackwener/opencli/errors';
7
+
8
+ export const OPENFDA_BASE = 'https://api.fda.gov';
9
+ const UA = 'opencli-openfda/1.0';
10
+
11
+ export function requireString(value, name) {
12
+ if (typeof value !== 'string' || !value.trim()) {
13
+ throw new ArgumentError(`--${name} is required`);
14
+ }
15
+ return value.trim();
16
+ }
17
+
18
+ export function requireBoundedInt(value, def, max, name = 'limit') {
19
+ const n = value == null || value === '' ? def : Number(value);
20
+ if (!Number.isInteger(n) || n < 1 || n > max) {
21
+ throw new ArgumentError(`--${name} must be an integer between 1 and ${max}`);
22
+ }
23
+ return n;
24
+ }
25
+
26
+ export async function openfdaFetch(url, label) {
27
+ let resp;
28
+ try {
29
+ resp = await fetch(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });
30
+ } catch (err) {
31
+ throw new CommandExecutionError(`${label} request failed: ${err.message}`);
32
+ }
33
+ if (resp.status === 404) {
34
+ // openFDA returns 404 for "no matches" instead of an empty results array.
35
+ throw new EmptyResultError(label, `${label} returned 404 (no matches).`);
36
+ }
37
+ if (resp.status === 429) {
38
+ throw new CommandExecutionError(`${label} rate-limited (HTTP 429); back off and retry.`);
39
+ }
40
+ if (!resp.ok) {
41
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}.`);
42
+ }
43
+ let body;
44
+ try {
45
+ body = await resp.json();
46
+ } catch (err) {
47
+ throw new CommandExecutionError(`${label} returned non-JSON body: ${err.message}`);
48
+ }
49
+ return body;
50
+ }
51
+
52
+ // openFDA returns most string fields as `[string]` arrays — collapse to first
53
+ // element. Preserves `null` (not coerced to empty string) when the slot is
54
+ // missing entirely.
55
+ export function firstOrNull(arr) {
56
+ if (!Array.isArray(arr) || !arr.length) return null;
57
+ const v = arr[0];
58
+ if (typeof v !== 'string') return v ?? null;
59
+ const trimmed = v.trim();
60
+ return trimmed.length ? trimmed : null;
61
+ }
62
+
63
+ // Comma-join an array of strings, preserving null when empty.
64
+ export function joinOrNull(arr, max = 5) {
65
+ if (!Array.isArray(arr) || !arr.length) return null;
66
+ return arr.slice(0, max).map(String).join(', ');
67
+ }
@@ -0,0 +1,97 @@
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 './vulnerability.js';
5
+ import './query.js';
6
+
7
+ afterEach(() => {
8
+ vi.unstubAllGlobals();
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ describe('osv vulnerability adapter', () => {
13
+ const cmd = getRegistry().get('osv/vulnerability');
14
+
15
+ it('rejects empty / malformed id before fetching', async () => {
16
+ const fetchMock = vi.fn();
17
+ vi.stubGlobal('fetch', fetchMock);
18
+
19
+ await expect(cmd.func({ id: '' })).rejects.toThrow(ArgumentError);
20
+ await expect(cmd.func({ id: 'has spaces' })).rejects.toThrow(ArgumentError);
21
+ expect(fetchMock).not.toHaveBeenCalled();
22
+ });
23
+
24
+ it('maps HTTP 404 to EmptyResultError', async () => {
25
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('not found', { status: 404 })));
26
+ await expect(cmd.func({ id: 'GHSA-aaaa-bbbb-cccc' })).rejects.toThrow(EmptyResultError);
27
+ });
28
+
29
+ it('returns flattened affected packages with severity from database_specific', async () => {
30
+ const vuln = {
31
+ id: 'GHSA-29mw-wpgm-hmr9',
32
+ summary: 'ReDoS in lodash',
33
+ aliases: ['CVE-2020-28500'],
34
+ published: '2022-01-06T20:30:46Z',
35
+ modified: '2025-09-29T21:12:31.102523Z',
36
+ database_specific: { severity: 'MODERATE', cwe_ids: ['CWE-1333', 'CWE-400'] },
37
+ severity: [{ type: 'CVSS_V3', score: 'CVSS:3.1/...' }],
38
+ affected: [
39
+ { package: { name: 'lodash', ecosystem: 'npm' } },
40
+ { package: { name: 'lodash-rails', ecosystem: 'RubyGems' } },
41
+ ],
42
+ references: [{ url: 'https://example' }],
43
+ };
44
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(vuln), { status: 200 })));
45
+
46
+ const rows = await cmd.func({ id: 'GHSA-29mw-wpgm-hmr9' });
47
+ expect(rows).toEqual([expect.objectContaining({
48
+ id: 'GHSA-29mw-wpgm-hmr9',
49
+ severity: 'MODERATE',
50
+ aliases: 'CVE-2020-28500',
51
+ affectedPackages: 'npm:lodash, RubyGems:lodash-rails',
52
+ cwes: 'CWE-1333, CWE-400',
53
+ referenceCount: 1,
54
+ modified: '2025-09-29T21:12:31Z',
55
+ })]);
56
+ });
57
+ });
58
+
59
+ describe('osv query adapter', () => {
60
+ const cmd = getRegistry().get('osv/query');
61
+
62
+ it('rejects bad ecosystem and bad limit before fetching', async () => {
63
+ const fetchMock = vi.fn();
64
+ vi.stubGlobal('fetch', fetchMock);
65
+
66
+ await expect(cmd.func({ package: 'lodash', ecosystem: 'foo', limit: 5 })).rejects.toThrow(ArgumentError);
67
+ await expect(cmd.func({ package: 'django', ecosystem: 'pypi', limit: 5 })).rejects.toThrow(ArgumentError);
68
+ await expect(cmd.func({ package: 'lodash', ecosystem: 'npm', limit: 0 })).rejects.toThrow(ArgumentError);
69
+ await expect(cmd.func({ package: '', ecosystem: 'npm', limit: 5 })).rejects.toThrow(ArgumentError);
70
+ expect(fetchMock).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('maps HTTP 429 to CommandExecutionError', async () => {
74
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('rate limited', { status: 429 })));
75
+ await expect(cmd.func({ package: 'lodash', ecosystem: 'npm', limit: 5 })).rejects.toThrow(CommandExecutionError);
76
+ });
77
+
78
+ it('throws EmptyResultError when no vulns reported', async () => {
79
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })));
80
+ await expect(cmd.func({ package: 'lodash', ecosystem: 'npm', limit: 5 })).rejects.toThrow(EmptyResultError);
81
+ });
82
+
83
+ it('sorts by published date descending and rows have id round-tripable into osv vulnerability', async () => {
84
+ const body = {
85
+ vulns: [
86
+ { id: 'GHSA-old', summary: 'old', published: '2020-01-01T00:00:00Z', affected: [{ package: { name: 'django', ecosystem: 'PyPI' } }] },
87
+ { id: 'GHSA-new', summary: 'new', published: '2026-01-01T00:00:00Z', affected: [{ package: { name: 'django', ecosystem: 'PyPI' } }] },
88
+ ],
89
+ };
90
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(body), { status: 200 })));
91
+
92
+ const rows = await cmd.func({ package: 'django', ecosystem: 'PyPI', limit: 5 });
93
+ expect(rows[0]).toMatchObject({ rank: 1, id: 'GHSA-new', published: '2026-01-01T00:00:00Z' });
94
+ expect(rows[1]).toMatchObject({ rank: 2, id: 'GHSA-old' });
95
+ expect(rows[0].url).toBe('https://osv.dev/vulnerability/GHSA-new');
96
+ });
97
+ });
@@ -0,0 +1,72 @@
1
+ // osv query — find vulnerabilities affecting a given package (and optional version).
2
+ //
3
+ // Hits `POST https://api.osv.dev/v1/query` with `{package:{name,ecosystem}, version?}`.
4
+ // Returns one row per vulnerability ranked by published date (newest first), so
5
+ // agents can answer "is package X@Y vulnerable?" in one shot.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { EmptyResultError } from '@jackwener/opencli/errors';
8
+ import {
9
+ OSV_BASE, osvPost, requireBoundedInt, requireEcosystem, requireString, severityLabel, trimDate,
10
+ } from './utils.js';
11
+
12
+ cli({
13
+ site: 'osv',
14
+ name: 'query',
15
+ access: 'read',
16
+ description: 'OSV.dev vulnerabilities affecting a package (optionally pinned to a version)',
17
+ domain: 'osv.dev',
18
+ strategy: Strategy.PUBLIC,
19
+ browser: false,
20
+ args: [
21
+ { name: 'package', positional: true, type: 'string', required: true, help: 'Package name (e.g. "lodash", "django")' },
22
+ { name: 'ecosystem', type: 'string', required: true, help: 'OSV ecosystem (npm / PyPI / Go / Maven / NuGet / RubyGems / crates.io / Packagist / ...)' },
23
+ { name: 'version', type: 'string', required: false, help: 'Pin to a specific version (e.g. "4.17.20"); omit for all known vulns' },
24
+ { name: 'limit', type: 'int', default: 30, help: 'Max rows to return (1-200)' },
25
+ ],
26
+ columns: [
27
+ 'rank', 'id', 'summary', 'severity', 'aliases',
28
+ 'published', 'modified', 'affectedPackages', 'url',
29
+ ],
30
+ func: async (args) => {
31
+ const name = requireString(args.package, 'package');
32
+ const ecosystem = requireEcosystem(args.ecosystem);
33
+ const limit = requireBoundedInt(args.limit, 30, 200, 'limit');
34
+ const payload = { package: { name, ecosystem } };
35
+ if (args.version != null && String(args.version).trim() !== '') {
36
+ payload.version = String(args.version).trim();
37
+ }
38
+ const body = await osvPost(`${OSV_BASE}/v1/query`, payload, `osv query ${ecosystem}:${name}`);
39
+ const vulns = Array.isArray(body?.vulns) ? body.vulns : [];
40
+ if (vulns.length === 0) {
41
+ throw new EmptyResultError(
42
+ 'osv query',
43
+ `OSV.dev returned no vulnerabilities for ${ecosystem}:${name}${payload.version ? `@${payload.version}` : ''}.`,
44
+ );
45
+ }
46
+ const sorted = vulns
47
+ .slice()
48
+ .sort((a, b) => String(b?.published ?? '').localeCompare(String(a?.published ?? '')))
49
+ .slice(0, limit);
50
+ return sorted.map((v, i) => {
51
+ const affected = Array.isArray(v.affected) ? v.affected : [];
52
+ const pkgPairs = [];
53
+ for (const a of affected) {
54
+ const eco = a?.package?.ecosystem;
55
+ const aname = a?.package?.name;
56
+ if (eco && aname) pkgPairs.push(`${eco}:${aname}`);
57
+ }
58
+ const aliases = Array.isArray(v.aliases) ? v.aliases.filter(Boolean) : [];
59
+ return {
60
+ rank: i + 1,
61
+ id: String(v.id ?? ''),
62
+ summary: String(v.summary ?? '').trim(),
63
+ severity: severityLabel(v),
64
+ aliases: aliases.join(', '),
65
+ published: trimDate(v.published),
66
+ modified: trimDate(v.modified),
67
+ affectedPackages: pkgPairs.join(', '),
68
+ url: v.id ? `https://osv.dev/vulnerability/${v.id}` : '',
69
+ };
70
+ });
71
+ },
72
+ });
@@ -0,0 +1,169 @@
1
+ // Shared helpers for the OSV.dev (Open Source Vulnerabilities) adapters.
2
+ //
3
+ // OSV.dev publishes a free, unauthenticated REST API at https://api.osv.dev.
4
+ // Docs: https://google.github.io/osv.dev/api/
5
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
6
+
7
+ export const OSV_BASE = 'https://api.osv.dev';
8
+ const UA = 'opencli-osv-adapter (+https://github.com/jackwener/opencli)';
9
+
10
+ // OSV vulnerability IDs are short tokens like "GHSA-29mw-wpgm-hmr9", "CVE-2020-28500", "PYSEC-2021-1".
11
+ const VULN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
12
+
13
+ // OSV ecosystems we accept on input. The full canonical list is at
14
+ // https://ossf.github.io/osv-schema/#defined-ecosystems — we accept the public
15
+ // ones that map cleanly to package registries opencli already supports.
16
+ export const OSV_ECOSYSTEMS = new Set([
17
+ 'npm',
18
+ 'PyPI',
19
+ 'Go',
20
+ 'Maven',
21
+ 'NuGet',
22
+ 'RubyGems',
23
+ 'crates.io',
24
+ 'Packagist',
25
+ 'Pub',
26
+ 'Hex',
27
+ 'Hackage',
28
+ 'CRAN',
29
+ 'Bitnami',
30
+ 'GitHub Actions',
31
+ 'SwiftURL',
32
+ ]);
33
+
34
+ export function requireString(value, label) {
35
+ const s = String(value ?? '').trim();
36
+ if (!s) throw new ArgumentError(`osv ${label} cannot be empty`);
37
+ return s;
38
+ }
39
+
40
+ export function requireVulnId(value) {
41
+ const s = String(value ?? '').trim();
42
+ if (!s) {
43
+ throw new ArgumentError(
44
+ 'osv vulnerability id is required (e.g. "GHSA-29mw-wpgm-hmr9", "CVE-2020-28500")',
45
+ 'IDs are listed at https://osv.dev — paste the canonical id from the vulnerability page.',
46
+ );
47
+ }
48
+ if (!VULN_ID.test(s)) {
49
+ throw new ArgumentError(
50
+ `osv vulnerability id "${value}" is not a valid OSV id`,
51
+ 'IDs are short ASCII tokens like "GHSA-...", "CVE-...", "PYSEC-...".',
52
+ );
53
+ }
54
+ return s;
55
+ }
56
+
57
+ export function requireEcosystem(value) {
58
+ const s = String(value ?? '').trim();
59
+ if (!s) {
60
+ throw new ArgumentError(
61
+ 'osv --ecosystem is required when querying by package',
62
+ `Pick one of: ${[...OSV_ECOSYSTEMS].join(', ')}.`,
63
+ );
64
+ }
65
+ if (!OSV_ECOSYSTEMS.has(s)) {
66
+ throw new ArgumentError(
67
+ `osv --ecosystem "${value}" is not a recognised OSV ecosystem`,
68
+ `Pick one of: ${[...OSV_ECOSYSTEMS].join(', ')}.`,
69
+ );
70
+ }
71
+ return s;
72
+ }
73
+
74
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
75
+ const raw = value ?? defaultValue;
76
+ const n = typeof raw === 'number' ? raw : Number(raw);
77
+ if (!Number.isInteger(n) || n <= 0) {
78
+ throw new ArgumentError(`osv ${label} must be a positive integer`);
79
+ }
80
+ if (n > maxValue) {
81
+ throw new ArgumentError(`osv ${label} must be <= ${maxValue}`);
82
+ }
83
+ return n;
84
+ }
85
+
86
+ async function readJson(resp, label) {
87
+ let body;
88
+ try {
89
+ body = await resp.json();
90
+ }
91
+ catch (err) {
92
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
93
+ }
94
+ return body;
95
+ }
96
+
97
+ export async function osvGet(url, label) {
98
+ let resp;
99
+ try {
100
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
101
+ }
102
+ catch (err) {
103
+ throw new CommandExecutionError(
104
+ `${label} request failed: ${err?.message ?? err}`,
105
+ 'Check that api.osv.dev is reachable from this network.',
106
+ );
107
+ }
108
+ if (resp.status === 404) {
109
+ throw new EmptyResultError(label, `OSV.dev returned 404 for ${url}.`);
110
+ }
111
+ if (resp.status === 429) {
112
+ throw new CommandExecutionError(`${label} returned HTTP 429 (rate limited)`);
113
+ }
114
+ if (!resp.ok) {
115
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
116
+ }
117
+ return readJson(resp, label);
118
+ }
119
+
120
+ export async function osvPost(url, payload, label) {
121
+ let resp;
122
+ try {
123
+ resp = await fetch(url, {
124
+ method: 'POST',
125
+ headers: { 'user-agent': UA, accept: 'application/json', 'content-type': 'application/json' },
126
+ body: JSON.stringify(payload),
127
+ });
128
+ }
129
+ catch (err) {
130
+ throw new CommandExecutionError(
131
+ `${label} request failed: ${err?.message ?? err}`,
132
+ 'Check that api.osv.dev is reachable from this network.',
133
+ );
134
+ }
135
+ if (resp.status === 404) {
136
+ throw new EmptyResultError(label, `OSV.dev returned 404 for ${url}.`);
137
+ }
138
+ if (resp.status === 429) {
139
+ throw new CommandExecutionError(`${label} returned HTTP 429 (rate limited)`);
140
+ }
141
+ if (!resp.ok) {
142
+ const text = await resp.text().catch(() => '');
143
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}${text ? `: ${text.slice(0, 200)}` : ''}`);
144
+ }
145
+ return readJson(resp, label);
146
+ }
147
+
148
+ // Reduce OSV's `severity` array to a single human-readable label.
149
+ // Returns null when no severity is recorded; never invents a value.
150
+ export function severityLabel(vuln) {
151
+ const dbSpecific = vuln?.database_specific;
152
+ if (dbSpecific && typeof dbSpecific.severity === 'string' && dbSpecific.severity.trim()) {
153
+ return dbSpecific.severity.trim();
154
+ }
155
+ const arr = Array.isArray(vuln?.severity) ? vuln.severity : [];
156
+ for (const entry of arr) {
157
+ if (entry && typeof entry.score === 'string' && entry.score.trim()) {
158
+ return entry.score.trim();
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+
164
+ export function trimDate(value) {
165
+ const s = String(value ?? '').trim();
166
+ if (!s) return null;
167
+ const noFrac = s.replace(/\.\d+/, '');
168
+ return noFrac.endsWith('Z') ? noFrac : (s.length >= 10 ? s.slice(0, 10) : null);
169
+ }
@@ -0,0 +1,54 @@
1
+ // osv vulnerability — fetch a single OSV vulnerability by id.
2
+ //
3
+ // Hits `https://api.osv.dev/v1/vulns/<id>`. Returns one row with the canonical
4
+ // id, summary, severity, aliases (CVE / GHSA mappings), publish / modify dates,
5
+ // and the affected packages (flattened to a comma-separated "ecosystem:name" list).
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { EmptyResultError } from '@jackwener/opencli/errors';
8
+ import { OSV_BASE, osvGet, requireVulnId, severityLabel, trimDate } from './utils.js';
9
+
10
+ cli({
11
+ site: 'osv',
12
+ name: 'vulnerability',
13
+ access: 'read',
14
+ description: 'Single OSV.dev vulnerability detail (severity, affected packages, CVE/GHSA aliases)',
15
+ domain: 'osv.dev',
16
+ strategy: Strategy.PUBLIC,
17
+ browser: false,
18
+ args: [
19
+ { name: 'id', positional: true, type: 'string', required: true, help: 'OSV vulnerability id (e.g. "GHSA-29mw-wpgm-hmr9", "CVE-2020-28500")' },
20
+ ],
21
+ columns: [
22
+ 'id', 'summary', 'severity', 'aliases', 'published', 'modified',
23
+ 'affectedPackages', 'cwes', 'referenceCount', 'url',
24
+ ],
25
+ func: async (args) => {
26
+ const id = requireVulnId(args.id);
27
+ const vuln = await osvGet(`${OSV_BASE}/v1/vulns/${encodeURIComponent(id)}`, `osv vulnerability ${id}`);
28
+ if (!vuln || !vuln.id) {
29
+ throw new EmptyResultError('osv vulnerability', `OSV.dev returned no record for "${id}".`);
30
+ }
31
+ const affected = Array.isArray(vuln.affected) ? vuln.affected : [];
32
+ const pkgPairs = [];
33
+ for (const a of affected) {
34
+ const eco = a?.package?.ecosystem;
35
+ const name = a?.package?.name;
36
+ if (eco && name) pkgPairs.push(`${eco}:${name}`);
37
+ }
38
+ const aliases = Array.isArray(vuln.aliases) ? vuln.aliases.filter(Boolean) : [];
39
+ const cwes = Array.isArray(vuln?.database_specific?.cwe_ids) ? vuln.database_specific.cwe_ids : [];
40
+ const refs = Array.isArray(vuln.references) ? vuln.references : [];
41
+ return [{
42
+ id: String(vuln.id),
43
+ summary: String(vuln.summary ?? '').trim(),
44
+ severity: severityLabel(vuln),
45
+ aliases: aliases.join(', '),
46
+ published: trimDate(vuln.published),
47
+ modified: trimDate(vuln.modified),
48
+ affectedPackages: pkgPairs.join(', '),
49
+ cwes: cwes.join(', '),
50
+ referenceCount: refs.length,
51
+ url: `https://osv.dev/vulnerability/${vuln.id}`,
52
+ }];
53
+ },
54
+ });
@@ -0,0 +1,49 @@
1
+ // packagist package — fetch a single Packagist package's metadata.
2
+ //
3
+ // Hits `https://packagist.org/packages/<vendor>/<package>.json`. Returns
4
+ // one row: latest stable version + release time, license, repository,
5
+ // description, lifetime / monthly / daily downloads, github stars, favers.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
8
+ import { PACKAGIST_BASE, packagistFetch, pickStableVersion, requirePackageName, trimDate } from './utils.js';
9
+
10
+ cli({
11
+ site: 'packagist',
12
+ name: 'package',
13
+ access: 'read',
14
+ description: 'Fetch a Packagist package\'s metadata (version, downloads, license, repo, GitHub stars)',
15
+ domain: 'packagist.org',
16
+ strategy: Strategy.PUBLIC,
17
+ browser: false,
18
+ args: [
19
+ { name: 'name', positional: true, required: true, help: 'Composer package "<vendor>/<package>" (e.g. "symfony/console", "monolog/monolog")' },
20
+ ],
21
+ columns: ['package', 'version', 'releasedAt', 'license', 'description', 'repository', 'githubStars', 'favers', 'downloads', 'monthlyDownloads', 'dailyDownloads', 'url'],
22
+ func: async (args) => {
23
+ const { full } = requirePackageName(args.name);
24
+ const url = `${PACKAGIST_BASE}/packages/${full}.json`;
25
+ const body = await packagistFetch(url, 'packagist package');
26
+ const pkg = body?.package;
27
+ if (!pkg || typeof pkg !== 'object') {
28
+ throw new CommandExecutionError(`packagist package returned no "package" object for ${full}.`);
29
+ }
30
+ const versionKey = pickStableVersion(pkg.versions);
31
+ const versionEntry = versionKey ? pkg.versions?.[versionKey] : null;
32
+ const license = Array.isArray(versionEntry?.license) ? versionEntry.license.filter(Boolean).join(', ') : '';
33
+ const downloads = pkg.downloads ?? {};
34
+ return [{
35
+ package: String(pkg.name ?? full).trim(),
36
+ version: versionKey ? String(versionKey) : '',
37
+ releasedAt: trimDate(versionEntry?.time),
38
+ license,
39
+ description: String(pkg.description ?? '').trim(),
40
+ repository: String(pkg.repository ?? '').trim(),
41
+ githubStars: pkg.github_stars != null ? Number(pkg.github_stars) : null,
42
+ favers: pkg.favers != null ? Number(pkg.favers) : null,
43
+ downloads: downloads.total != null ? Number(downloads.total) : null,
44
+ monthlyDownloads: downloads.monthly != null ? Number(downloads.monthly) : null,
45
+ dailyDownloads: downloads.daily != null ? Number(downloads.daily) : null,
46
+ url: `https://packagist.org/packages/${full}`,
47
+ }];
48
+ },
49
+ });
@@ -0,0 +1,43 @@
1
+ // packagist search — search Packagist's PHP / Composer package registry.
2
+ //
3
+ // Hits `https://packagist.org/search.json?q=…&per_page=…`. Returns the
4
+ // agent-useful projection: vendor/package (round-trips into `packagist
5
+ // package`), description, lifetime download count, GitHub-stars-style favers,
6
+ // repository URL.
7
+ import { cli, Strategy } from '@jackwener/opencli/registry';
8
+ import { EmptyResultError } from '@jackwener/opencli/errors';
9
+ import { PACKAGIST_BASE, packagistFetch, requireBoundedInt, requireString } from './utils.js';
10
+
11
+ cli({
12
+ site: 'packagist',
13
+ name: 'search',
14
+ access: 'read',
15
+ description: 'Search Packagist (PHP / Composer) packages by keyword',
16
+ domain: 'packagist.org',
17
+ strategy: Strategy.PUBLIC,
18
+ browser: false,
19
+ args: [
20
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "symfony", "laravel http")' },
21
+ { name: 'limit', type: 'int', default: 30, help: 'Max packages (1-100, single Packagist page)' },
22
+ ],
23
+ columns: ['rank', 'package', 'description', 'downloads', 'favers', 'repository', 'url'],
24
+ func: async (args) => {
25
+ const query = requireString(args.query, 'query');
26
+ const limit = requireBoundedInt(args.limit, 30, 100);
27
+ const url = `${PACKAGIST_BASE}/search.json?q=${encodeURIComponent(query)}&per_page=${limit}`;
28
+ const body = await packagistFetch(url, 'packagist search');
29
+ const list = Array.isArray(body?.results) ? body.results : [];
30
+ if (!list.length) {
31
+ throw new EmptyResultError('packagist search', `No Packagist packages matched "${query}".`);
32
+ }
33
+ return list.slice(0, limit).map((row, i) => ({
34
+ rank: i + 1,
35
+ package: String(row.name ?? '').trim(),
36
+ description: String(row.description ?? '').trim(),
37
+ downloads: row.downloads != null ? Number(row.downloads) : null,
38
+ favers: row.favers != null ? Number(row.favers) : null,
39
+ repository: String(row.repository ?? '').trim(),
40
+ url: String(row.url ?? '').trim(),
41
+ }));
42
+ },
43
+ });
@@ -0,0 +1,113 @@
1
+ // Shared helpers for the Packagist (PHP / Composer) adapters.
2
+ //
3
+ // Hits the public, unauthenticated `packagist.org` JSON endpoints. Composer's
4
+ // canonical package registry. Package names are `<vendor>/<package>`,
5
+ // lowercase letters / digits / `_-.`, with each segment 1-100 chars.
6
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
7
+
8
+ export const PACKAGIST_BASE = 'https://packagist.org';
9
+ const UA = 'opencli-packagist-adapter (+https://github.com/jackwener/opencli)';
10
+
11
+ // Each segment of a Composer package name (`vendor` and `package`).
12
+ const SEGMENT = /^[a-z0-9]([_.-]?[a-z0-9]+)*$/;
13
+
14
+ export function requireString(value, label) {
15
+ const s = String(value ?? '').trim();
16
+ if (!s) throw new ArgumentError(`packagist ${label} cannot be empty`);
17
+ return s;
18
+ }
19
+
20
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
21
+ const raw = value ?? defaultValue;
22
+ const n = typeof raw === 'number' ? raw : Number(raw);
23
+ if (!Number.isInteger(n) || n <= 0) {
24
+ throw new ArgumentError(`packagist ${label} must be a positive integer`);
25
+ }
26
+ if (n > maxValue) {
27
+ throw new ArgumentError(`packagist ${label} must be <= ${maxValue}`);
28
+ }
29
+ return n;
30
+ }
31
+
32
+ export function requirePackageName(value) {
33
+ const raw = String(value ?? '').trim().toLowerCase();
34
+ if (!raw) {
35
+ throw new ArgumentError('packagist package name is required (e.g. "symfony/console", "monolog/monolog")');
36
+ }
37
+ const slash = raw.indexOf('/');
38
+ if (slash <= 0 || slash === raw.length - 1) {
39
+ throw new ArgumentError(
40
+ `packagist package "${value}" must be "<vendor>/<package>"`,
41
+ 'Both segments are required (Composer convention).',
42
+ );
43
+ }
44
+ const vendor = raw.slice(0, slash);
45
+ const pkg = raw.slice(slash + 1);
46
+ if (vendor.length > 100 || pkg.length > 100 || !SEGMENT.test(vendor) || !SEGMENT.test(pkg)) {
47
+ throw new ArgumentError(
48
+ `packagist package "${value}" is not a valid Composer name`,
49
+ 'Use lowercase letters / digits / "_-.", segments separated by single "_-." chars (max 100 chars each).',
50
+ );
51
+ }
52
+ return { vendor, package: pkg, full: `${vendor}/${pkg}` };
53
+ }
54
+
55
+ export async function packagistFetch(url, label) {
56
+ let resp;
57
+ try {
58
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
59
+ }
60
+ catch (err) {
61
+ throw new CommandExecutionError(
62
+ `${label} request failed: ${err?.message ?? err}`,
63
+ 'Check that packagist.org is reachable from this network.',
64
+ );
65
+ }
66
+ if (resp.status === 404) {
67
+ throw new EmptyResultError(label, `Packagist returned 404 for ${url}.`);
68
+ }
69
+ if (resp.status === 429) {
70
+ throw new CommandExecutionError(
71
+ `${label} returned HTTP 429 (rate limited)`,
72
+ 'Packagist throttles bursts; wait a few seconds and retry.',
73
+ );
74
+ }
75
+ if (!resp.ok) {
76
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
77
+ }
78
+ let body;
79
+ try {
80
+ body = await resp.json();
81
+ }
82
+ catch (err) {
83
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
84
+ }
85
+ return body;
86
+ }
87
+
88
+ /** Trim "2026-05-05T17:32:01+00:00" → "2026-05-05T17:32:01Z" so timestamps are uniform. */
89
+ export function trimDate(value) {
90
+ const s = String(value ?? '').trim();
91
+ if (!s) return null;
92
+ const noFrac = s.replace(/\.\d+/, '');
93
+ return noFrac.replace(/(?:[+-]\d{2}:?\d{2}|Z)?$/, 'Z');
94
+ }
95
+
96
+ /**
97
+ * Pick the newest stable (non-dev / non-prerelease) version key from a
98
+ * Packagist `versions` map. Packagist returns keys ordered newest-first.
99
+ * Falls back to the first key if no stable found.
100
+ */
101
+ export function pickStableVersion(versions) {
102
+ if (!versions || typeof versions !== 'object') return null;
103
+ const keys = Object.keys(versions);
104
+ if (!keys.length) return null;
105
+ const PRE = /(?:^|[._\-+])(?:dev|alpha|beta|rc|pre|nightly)(?:[._\-+\d]|$)/i;
106
+ const DEV_SUFFIX = /\.x-dev$|-dev$/i;
107
+ for (const k of keys) {
108
+ if (DEV_SUFFIX.test(k)) continue;
109
+ if (PRE.test(k)) continue;
110
+ return k;
111
+ }
112
+ return keys[0];
113
+ }