@jackwener/opencli 1.7.12 → 1.7.13

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 (407) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12194 -6843
  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/reply-dm.js +1 -1
  298. package/clis/twitter/reply.test.js +1 -29
  299. package/clis/uisdc/news.js +105 -0
  300. package/clis/uisdc/news.test.js +66 -0
  301. package/clis/wanfang/search.js +0 -1
  302. package/clis/web/read.js +47 -17
  303. package/clis/web/read.test.js +101 -1
  304. package/clis/weixin/create-draft.js +1 -1
  305. package/clis/weixin/drafts.js +1 -1
  306. package/clis/weixin/drafts.test.js +5 -1
  307. package/clis/weixin/search.js +157 -0
  308. package/clis/weixin/search.test.js +227 -0
  309. package/clis/wikidata/entity.js +60 -0
  310. package/clis/wikidata/search.js +50 -0
  311. package/clis/wikidata/utils.js +117 -0
  312. package/clis/wikidata/wikidata.test.js +83 -0
  313. package/clis/wikipedia/page.js +95 -0
  314. package/clis/wttr/current.js +63 -0
  315. package/clis/wttr/forecast.js +71 -0
  316. package/clis/wttr/utils.js +50 -0
  317. package/clis/wttr/wttr.test.js +84 -0
  318. package/clis/xianyu/chat.js +16 -4
  319. package/clis/xianyu/chat.test.js +64 -0
  320. package/clis/xianyu/publish.js +485 -0
  321. package/clis/xianyu/publish.test.js +220 -0
  322. package/clis/xiaoe/catalog.js +105 -40
  323. package/clis/xiaoe/content.js +164 -29
  324. package/clis/xiaoe/courses.js +86 -29
  325. package/clis/xiaoe/xiaoe.test.js +486 -0
  326. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  327. package/clis/xiaohongshu/publish.js +16 -3
  328. package/clis/xiaohongshu/publish.test.js +46 -1
  329. package/clis/youtube/transcript.js +13 -19
  330. package/clis/youtube/transcript.test.js +17 -0
  331. package/clis/yuanbao/ask.js +17 -66
  332. package/clis/yuanbao/ask.test.js +5 -5
  333. package/clis/yuanbao/detail.js +65 -0
  334. package/clis/yuanbao/history.js +51 -0
  335. package/clis/yuanbao/new.js +1 -0
  336. package/clis/yuanbao/read.js +38 -0
  337. package/clis/yuanbao/send.js +57 -0
  338. package/clis/yuanbao/shared.js +297 -5
  339. package/clis/yuanbao/shared.test.js +80 -0
  340. package/clis/yuanbao/status.js +44 -0
  341. package/clis/zlibrary/commands.test.js +1 -11
  342. package/dist/src/browser/base-page.d.ts +9 -0
  343. package/dist/src/browser/base-page.js +44 -1
  344. package/dist/src/browser/base-page.test.js +66 -0
  345. package/dist/src/browser/cdp.d.ts +1 -0
  346. package/dist/src/browser/cdp.js +51 -9
  347. package/dist/src/browser/daemon-client.d.ts +4 -0
  348. package/dist/src/browser/errors.js +1 -1
  349. package/dist/src/browser/page.d.ts +1 -1
  350. package/dist/src/browser/page.js +3 -1
  351. package/dist/src/browser/page.test.js +29 -0
  352. package/dist/src/browser/target-errors.d.ts +2 -1
  353. package/dist/src/browser/target-errors.js +1 -0
  354. package/dist/src/browser/target-resolver.d.ts +25 -0
  355. package/dist/src/browser/target-resolver.js +43 -0
  356. package/dist/src/build-manifest.js +9 -4
  357. package/dist/src/build-manifest.test.js +2 -8
  358. package/dist/src/capabilityRouting.d.ts +16 -1
  359. package/dist/src/capabilityRouting.js +24 -1
  360. package/dist/src/capabilityRouting.test.js +19 -1
  361. package/dist/src/cli.js +76 -11
  362. package/dist/src/cli.test.js +150 -0
  363. package/dist/src/commanderAdapter.js +0 -5
  364. package/dist/src/commanderAdapter.test.js +0 -1
  365. package/dist/src/discovery.js +2 -5
  366. package/dist/src/errors.js +1 -1
  367. package/dist/src/execution.d.ts +1 -1
  368. package/dist/src/execution.js +111 -27
  369. package/dist/src/execution.test.js +326 -17
  370. package/dist/src/help.d.ts +23 -2
  371. package/dist/src/help.js +41 -19
  372. package/dist/src/help.test.d.ts +1 -0
  373. package/dist/src/help.test.js +54 -0
  374. package/dist/src/main.js +14 -1
  375. package/dist/src/manifest-types.d.ts +5 -3
  376. package/dist/src/pipeline/executor.js +1 -1
  377. package/dist/src/pipeline/executor.test.js +8 -0
  378. package/dist/src/pipeline/registry.d.ts +9 -0
  379. package/dist/src/pipeline/registry.js +13 -1
  380. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  381. package/dist/src/pipeline/steps/browser.js +10 -0
  382. package/dist/src/pipeline/steps/download.test.js +1 -0
  383. package/dist/src/registry-api.d.ts +1 -1
  384. package/dist/src/registry.d.ts +12 -11
  385. package/dist/src/registry.js +16 -6
  386. package/dist/src/registry.test.js +2 -2
  387. package/dist/src/runtime.d.ts +2 -1
  388. package/dist/src/runtime.js +1 -1
  389. package/dist/src/serialization.d.ts +2 -2
  390. package/dist/src/serialization.js +4 -6
  391. package/dist/src/serialization.test.js +17 -0
  392. package/dist/src/types.d.ts +17 -0
  393. package/dist/src/validate.js +15 -11
  394. package/dist/src/validate.test.d.ts +9 -0
  395. package/dist/src/validate.test.js +90 -0
  396. package/package.json +1 -1
  397. package/scripts/fetch-adapters.js +1 -1
  398. package/scripts/typed-error-lint-baseline.json +5 -77
  399. package/clis/ctrip/search.test.js +0 -64
  400. package/clis/gov-policy/commands.test.js +0 -27
  401. package/clis/linux-do/category.js +0 -37
  402. package/clis/linux-do/hot.js +0 -26
  403. package/clis/linux-do/latest.js +0 -19
  404. package/clis/pixiv/test-utils.js +0 -23
  405. package/clis/toutiao/articles.test.js +0 -30
  406. package/dist/src/analysis.d.ts +0 -40
  407. package/dist/src/analysis.js +0 -172
@@ -0,0 +1,55 @@
1
+ // defillama protocols — top DeFi protocols by current TVL.
2
+ //
3
+ // Hits `https://api.llama.fi/protocols`, sorts by TVL (desc), and returns the
4
+ // requested top-N. The API ships ~7400 entries today; we cap output at 500
5
+ // rows so agents do not paginate their entire DeFi universe by accident.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { EmptyResultError } from '@jackwener/opencli/errors';
8
+ import { LLAMA_BASE, llamaFetch, requireBoundedInt, unixToDate } from './utils.js';
9
+
10
+ cli({
11
+ site: 'defillama',
12
+ name: 'protocols',
13
+ access: 'read',
14
+ description: 'Top DeFi protocols on DefiLlama by current TVL (slug, name, category, TVL, mcap, change_1d/7d, chains)',
15
+ domain: 'defillama.com',
16
+ strategy: Strategy.PUBLIC,
17
+ browser: false,
18
+ args: [
19
+ { name: 'limit', type: 'int', default: 30, help: 'Number of rows to return (1-500)' },
20
+ ],
21
+ columns: [
22
+ 'rank', 'slug', 'name', 'category', 'tvl', 'mcap',
23
+ 'change_1d', 'change_7d', 'chains', 'listedAt', 'url',
24
+ ],
25
+ func: async (args) => {
26
+ const limit = requireBoundedInt(args.limit, 30, 500, 'limit');
27
+ const list = await llamaFetch(`${LLAMA_BASE}/protocols`, 'defillama protocols');
28
+ if (!Array.isArray(list) || list.length === 0) {
29
+ throw new EmptyResultError('defillama protocols', 'DefiLlama returned no protocol entries.');
30
+ }
31
+ const ranked = list
32
+ .filter((p) => p && (p.slug || p.name) && Number.isFinite(Number(p.tvl)))
33
+ .sort((a, b) => Number(b.tvl) - Number(a.tvl))
34
+ .slice(0, limit);
35
+ if (ranked.length === 0) {
36
+ throw new EmptyResultError('defillama protocols', 'DefiLlama returned no protocols with numeric TVL.');
37
+ }
38
+ return ranked.map((p, i) => {
39
+ const slug = String(p.slug ?? '').trim();
40
+ return {
41
+ rank: i + 1,
42
+ slug,
43
+ name: String(p.name ?? '').trim(),
44
+ category: String(p.category ?? '').trim(),
45
+ tvl: Number(p.tvl),
46
+ mcap: p.mcap == null ? null : Number(p.mcap),
47
+ change_1d: p.change_1d == null ? null : Number(p.change_1d),
48
+ change_7d: p.change_7d == null ? null : Number(p.change_7d),
49
+ chains: Array.isArray(p.chains) ? p.chains.join(', ') : '',
50
+ listedAt: unixToDate(p.listedAt),
51
+ url: slug ? `https://defillama.com/protocol/${slug}` : '',
52
+ };
53
+ });
54
+ },
55
+ });
@@ -0,0 +1,99 @@
1
+ // Shared helpers for the DefiLlama adapters.
2
+ //
3
+ // DefiLlama serves a public REST API (no auth) over https://api.llama.fi.
4
+ // Docs: https://defillama.com/docs/api
5
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
6
+
7
+ export const LLAMA_BASE = 'https://api.llama.fi';
8
+ const UA = 'opencli-defillama-adapter (+https://github.com/jackwener/opencli)';
9
+
10
+ // DefiLlama slugs are lowercase with hyphens / digits / dots; allow up to 100 chars.
11
+ const SLUG = /^[a-z0-9][a-z0-9._-]{0,99}$/;
12
+
13
+ export function requireString(value, label) {
14
+ const s = String(value ?? '').trim();
15
+ if (!s) throw new ArgumentError(`defillama ${label} cannot be empty`);
16
+ return s;
17
+ }
18
+
19
+ export function requireSlug(value, label = 'slug') {
20
+ const s = String(value ?? '').trim();
21
+ if (!s) {
22
+ throw new ArgumentError(
23
+ `defillama ${label} is required (e.g. "aave", "lido")`,
24
+ 'Use the protocol slug as it appears on https://defillama.com/protocol/<slug>.',
25
+ );
26
+ }
27
+ if (!SLUG.test(s)) {
28
+ throw new ArgumentError(
29
+ `defillama ${label} "${value}" is not a valid DefiLlama slug`,
30
+ 'Slugs are lowercase ASCII (letters / digits / "._-"), e.g. "aave", "lido", "pancakeswap-amm".',
31
+ );
32
+ }
33
+ return s;
34
+ }
35
+
36
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
37
+ const raw = value ?? defaultValue;
38
+ const n = typeof raw === 'number' ? raw : Number(raw);
39
+ if (!Number.isInteger(n) || n <= 0) {
40
+ throw new ArgumentError(`defillama ${label} must be a positive integer`);
41
+ }
42
+ if (n > maxValue) {
43
+ throw new ArgumentError(`defillama ${label} must be <= ${maxValue}`);
44
+ }
45
+ return n;
46
+ }
47
+
48
+ export async function llamaFetch(url, label) {
49
+ let resp;
50
+ try {
51
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
52
+ }
53
+ catch (err) {
54
+ throw new CommandExecutionError(
55
+ `${label} request failed: ${err?.message ?? err}`,
56
+ 'Check that api.llama.fi is reachable from this network.',
57
+ );
58
+ }
59
+ if (resp.status === 404) {
60
+ throw new EmptyResultError(label, `DefiLlama returned 404 for ${url}.`);
61
+ }
62
+ if (resp.status === 400) {
63
+ // DefiLlama returns 400 + plain-text "Protocol not found" for unknown slugs.
64
+ const body = await resp.text().catch(() => '');
65
+ if (/not\s*found/i.test(body)) {
66
+ throw new EmptyResultError(label, `DefiLlama: ${body.trim() || 'not found'}.`);
67
+ }
68
+ throw new CommandExecutionError(`${label} returned HTTP 400: ${body.slice(0, 200)}`);
69
+ }
70
+ if (resp.status === 429) {
71
+ throw new CommandExecutionError(
72
+ `${label} returned HTTP 429 (rate limited)`,
73
+ 'DefiLlama throttles unauthenticated traffic; wait a few seconds and retry.',
74
+ );
75
+ }
76
+ if (!resp.ok) {
77
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
78
+ }
79
+ let body;
80
+ try {
81
+ body = await resp.json();
82
+ }
83
+ catch (err) {
84
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
85
+ }
86
+ return body;
87
+ }
88
+
89
+ // Convert a unix-seconds timestamp (DefiLlama's listedAt convention) to YYYY-MM-DD,
90
+ // or null when the value is missing / not a finite number.
91
+ export function unixToDate(value) {
92
+ if (value == null) return null;
93
+ const n = typeof value === 'number' ? value : Number(value);
94
+ if (!Number.isFinite(n) || n <= 0) return null;
95
+ const ms = n > 1e12 ? n : n * 1000;
96
+ const d = new Date(ms);
97
+ if (Number.isNaN(d.getTime())) return null;
98
+ return d.toISOString().slice(0, 10);
99
+ }
@@ -0,0 +1,74 @@
1
+ // devto latest — newest dev.to articles, regardless of tag.
2
+ //
3
+ // Hits the public `/api/articles/latest` endpoint. Complements the existing
4
+ // `devto top` (most-reactioned) and `devto tag` (filtered) commands by
5
+ // surfacing the firehose of brand-new posts.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
8
+
9
+ function requireBoundedInt(value, defaultValue, maxValue) {
10
+ const raw = value ?? defaultValue;
11
+ const n = typeof raw === 'number' ? raw : Number(raw);
12
+ if (!Number.isInteger(n) || n <= 0) {
13
+ throw new ArgumentError('devto limit must be a positive integer');
14
+ }
15
+ if (n > maxValue) {
16
+ throw new ArgumentError(`devto limit must be <= ${maxValue}`);
17
+ }
18
+ return n;
19
+ }
20
+
21
+ cli({
22
+ site: 'devto',
23
+ name: 'latest',
24
+ access: 'read',
25
+ description: 'Newest dev.to articles (firehose, all tags)',
26
+ domain: 'dev.to',
27
+ strategy: Strategy.PUBLIC,
28
+ browser: false,
29
+ args: [
30
+ { name: 'limit', type: 'int', default: 20, help: 'Articles per page (1-100)' },
31
+ { name: 'page', type: 'int', default: 1, help: 'Page number (1-based)' },
32
+ ],
33
+ columns: ['rank', 'id', 'title', 'author', 'tags', 'reactions', 'comments', 'published', 'url'],
34
+ func: async (args) => {
35
+ const limit = requireBoundedInt(args.limit, 20, 100);
36
+ const page = requireBoundedInt(args.page, 1, 1000);
37
+ const url = `https://dev.to/api/articles/latest?per_page=${limit}&page=${page}`;
38
+ let resp;
39
+ try {
40
+ resp = await fetch(url, { headers: { accept: 'application/json' } });
41
+ }
42
+ catch (err) {
43
+ throw new CommandExecutionError(
44
+ `devto latest request failed: ${err?.message ?? err}`,
45
+ 'Check that dev.to is reachable from this network.',
46
+ );
47
+ }
48
+ if (!resp.ok) {
49
+ throw new CommandExecutionError(`devto latest returned HTTP ${resp.status}`);
50
+ }
51
+ let body;
52
+ try {
53
+ body = await resp.json();
54
+ }
55
+ catch (err) {
56
+ throw new CommandExecutionError(`devto latest returned malformed JSON: ${err?.message ?? err}`);
57
+ }
58
+ const list = Array.isArray(body) ? body : [];
59
+ if (!list.length) {
60
+ throw new EmptyResultError('devto latest', `dev.to /articles/latest returned no items at page ${page}.`);
61
+ }
62
+ return list.map((item, i) => ({
63
+ rank: (page - 1) * limit + i + 1,
64
+ id: item.id != null ? String(item.id) : '',
65
+ title: String(item.title ?? ''),
66
+ author: String(item?.user?.username ?? ''),
67
+ tags: String(item.tag_list ?? '').replace(/,\s*/g, ', '),
68
+ reactions: item.public_reactions_count != null ? Number(item.public_reactions_count) : null,
69
+ comments: item.comments_count != null ? Number(item.comments_count) : null,
70
+ published: String(item.published_at ?? '').slice(0, 10),
71
+ url: String(item.url ?? ''),
72
+ }));
73
+ },
74
+ });
@@ -0,0 +1,52 @@
1
+ // dockerhub image — fetch a single Docker Hub repository's metadata.
2
+ //
3
+ // Hits `https://hub.docker.com/v2/repositories/<owner>/<name>`. Bare names
4
+ // resolve to the implicit `library` owner used for Docker official images
5
+ // (so `dockerhub image nginx` ≡ `dockerhub image library/nginx`). Returns a
6
+ // one-row projection: official-flag, star / pull counters, last-updated /
7
+ // registered timestamps, repo status, short description, hub URL.
8
+ import { cli, Strategy } from '@jackwener/opencli/registry';
9
+ import { HUB_BASE, hubFetch, parseImage } from './utils.js';
10
+
11
+ function trimDate(value) {
12
+ const s = String(value ?? '').trim();
13
+ if (!s) return null;
14
+ // Docker Hub returns mixed precision (`...45Z` and `...35.286495Z`). Drop
15
+ // the fractional part so all timestamp columns share `YYYY-MM-DDTHH:MM:SSZ`.
16
+ const noFrac = s.replace(/\.\d+/, '');
17
+ return noFrac.endsWith('Z') ? noFrac : `${noFrac}Z`;
18
+ }
19
+
20
+ cli({
21
+ site: 'dockerhub',
22
+ name: 'image',
23
+ access: 'read',
24
+ description: 'Fetch a Docker Hub repository\'s public metadata (stars, pulls, last updated, status)',
25
+ domain: 'hub.docker.com',
26
+ strategy: Strategy.PUBLIC,
27
+ browser: false,
28
+ args: [
29
+ { name: 'image', positional: true, required: true, help: 'Image name (e.g. "nginx", "library/nginx", "bitnami/redis")' },
30
+ ],
31
+ columns: ['image', 'official', 'stars', 'pulls', 'description', 'lastUpdated', 'lastModified', 'registered', 'status', 'url'],
32
+ func: async (args) => {
33
+ const { owner, name } = parseImage(args.image);
34
+ const url = `${HUB_BASE}/repositories/${owner}/${name}/`;
35
+ const body = await hubFetch(url, 'dockerhub image');
36
+ const namespace = String(body?.namespace ?? owner).trim();
37
+ const isOfficial = namespace === 'library' || namespace === '_';
38
+ const image = isOfficial ? `library/${name}` : `${namespace}/${name}`;
39
+ return [{
40
+ image,
41
+ official: isOfficial,
42
+ stars: body?.star_count != null ? Number(body.star_count) : null,
43
+ pulls: body?.pull_count != null ? Number(body.pull_count) : null,
44
+ description: String(body?.description ?? '').trim(),
45
+ lastUpdated: trimDate(body?.last_updated),
46
+ lastModified: trimDate(body?.last_modified),
47
+ registered: trimDate(body?.date_registered),
48
+ status: String(body?.status_description ?? '').trim(),
49
+ url: `https://hub.docker.com/r/${image}`,
50
+ }];
51
+ },
52
+ });
@@ -0,0 +1,47 @@
1
+ // dockerhub search — search the public Docker Hub repository index.
2
+ //
3
+ // Hits `https://hub.docker.com/v2/search/repositories/?query=…`. Returns the
4
+ // agent-useful projection: official-flag, owner/name (round-trips into
5
+ // `dockerhub image`), star count, pull count, short description.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { EmptyResultError } from '@jackwener/opencli/errors';
8
+ import { HUB_BASE, hubFetch, requireBoundedInt, requireString } from './utils.js';
9
+
10
+ cli({
11
+ site: 'dockerhub',
12
+ name: 'search',
13
+ access: 'read',
14
+ description: 'Search Docker Hub repositories by keyword',
15
+ domain: 'hub.docker.com',
16
+ strategy: Strategy.PUBLIC,
17
+ browser: false,
18
+ args: [
19
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "nginx", "bitnami redis")' },
20
+ { name: 'limit', type: 'int', default: 25, help: 'Max repositories (1-100, single Docker Hub page)' },
21
+ ],
22
+ columns: ['rank', 'image', 'official', 'stars', 'pulls', 'description', 'url'],
23
+ func: async (args) => {
24
+ const query = requireString(args.query, 'query');
25
+ const limit = requireBoundedInt(args.limit, 25, 100);
26
+ const url = `${HUB_BASE}/search/repositories/?query=${encodeURIComponent(query)}&page_size=${limit}`;
27
+ const body = await hubFetch(url, 'dockerhub search');
28
+ const list = Array.isArray(body?.results) ? body.results : [];
29
+ if (!list.length) {
30
+ throw new EmptyResultError('dockerhub search', `No Docker Hub repositories matched "${query}".`);
31
+ }
32
+ return list.slice(0, limit).map((r, i) => {
33
+ const owner = String(r.repo_owner ?? '').trim();
34
+ const name = String(r.repo_name ?? '').trim();
35
+ const image = owner ? `${owner}/${name}` : (r.is_official ? `library/${name}` : name);
36
+ return {
37
+ rank: i + 1,
38
+ image,
39
+ official: Boolean(r.is_official),
40
+ stars: r.star_count != null ? Number(r.star_count) : null,
41
+ pulls: r.pull_count != null ? Number(r.pull_count) : null,
42
+ description: String(r.short_description ?? '').trim(),
43
+ url: image ? `https://hub.docker.com/r/${image}` : '',
44
+ };
45
+ });
46
+ },
47
+ });
@@ -0,0 +1,100 @@
1
+ // Shared helpers for the Docker Hub adapters.
2
+ //
3
+ // Hits the public, unauthenticated `hub.docker.com/v2` REST endpoints. Anonymous
4
+ // pulls are throttled but search / metadata reads are friendly enough for
5
+ // ad-hoc CLI use. Image names follow `[<owner>/]<name>` with `library` as the
6
+ // implicit owner for Docker official images.
7
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
8
+
9
+ export const HUB_BASE = 'https://hub.docker.com/v2';
10
+ const UA = 'opencli-dockerhub-adapter (+https://github.com/jackwener/opencli)';
11
+
12
+ // Docker Hub repository slugs are 2-255 chars, lowercase alphanumerics + `_.-`,
13
+ // optionally prefixed with a Docker Hub user/org of the same charset.
14
+ const SLUG = /^[a-z0-9][a-z0-9._-]*$/;
15
+
16
+ export function requireString(value, label) {
17
+ const s = String(value ?? '').trim();
18
+ if (!s) throw new ArgumentError(`dockerhub ${label} cannot be empty`);
19
+ return s;
20
+ }
21
+
22
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
23
+ const raw = value ?? defaultValue;
24
+ const n = typeof raw === 'number' ? raw : Number(raw);
25
+ if (!Number.isInteger(n) || n <= 0) {
26
+ throw new ArgumentError(`dockerhub ${label} must be a positive integer`);
27
+ }
28
+ if (n > maxValue) {
29
+ throw new ArgumentError(`dockerhub ${label} must be <= ${maxValue}`);
30
+ }
31
+ return n;
32
+ }
33
+
34
+ /**
35
+ * Split an image identifier into `{owner, name}`. Bare names use the implicit
36
+ * `library` owner that Docker Hub uses for official images (`nginx` →
37
+ * `library/nginx`).
38
+ */
39
+ export function parseImage(input) {
40
+ const raw = String(input ?? '').trim().toLowerCase();
41
+ if (!raw) {
42
+ throw new ArgumentError('dockerhub image name is required (e.g. "nginx", "library/nginx", "bitnami/redis")');
43
+ }
44
+ const slash = raw.indexOf('/');
45
+ let owner;
46
+ let name;
47
+ if (slash >= 0) {
48
+ owner = raw.slice(0, slash);
49
+ name = raw.slice(slash + 1);
50
+ }
51
+ else {
52
+ owner = 'library';
53
+ name = raw;
54
+ }
55
+ if (!SLUG.test(owner) || !SLUG.test(name)) {
56
+ throw new ArgumentError(
57
+ `dockerhub image "${input}" is not a valid repository slug`,
58
+ 'Use lowercase letters / digits / "._-", optionally prefixed with "<owner>/".',
59
+ );
60
+ }
61
+ if (name.length < 2 || name.length > 255) {
62
+ throw new ArgumentError(
63
+ `dockerhub image "${input}" name must be 2-255 chars`,
64
+ );
65
+ }
66
+ return { owner, name };
67
+ }
68
+
69
+ export async function hubFetch(url, label) {
70
+ let resp;
71
+ try {
72
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
73
+ }
74
+ catch (err) {
75
+ throw new CommandExecutionError(
76
+ `${label} request failed: ${err?.message ?? err}`,
77
+ 'Check that hub.docker.com is reachable from this network.',
78
+ );
79
+ }
80
+ if (resp.status === 404) {
81
+ throw new EmptyResultError(label, `Docker Hub returned 404 for ${url}.`);
82
+ }
83
+ if (resp.status === 429) {
84
+ throw new CommandExecutionError(
85
+ `${label} returned HTTP 429 (rate limited)`,
86
+ 'Docker Hub throttles anonymous traffic; wait a few seconds and retry.',
87
+ );
88
+ }
89
+ if (!resp.ok) {
90
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
91
+ }
92
+ let body;
93
+ try {
94
+ body = await resp.json();
95
+ }
96
+ catch (err) {
97
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
98
+ }
99
+ return body;
100
+ }
@@ -1,4 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError } from '@jackwener/opencli/errors';
2
3
  import { DOUBAO_DOMAIN, getDoubaoTranscriptLines, getDoubaoVisibleTurns, sendDoubaoMessage, waitForDoubaoResponse } from './utils.js';
3
4
  export const askCommand = cli({
4
5
  site: 'doubao',
@@ -8,16 +9,19 @@ export const askCommand = cli({
8
9
  domain: DOUBAO_DOMAIN,
9
10
  strategy: Strategy.COOKIE,
10
11
  browser: true,
12
+ browserSession: { reuse: 'site' },
11
13
  navigateBefore: false,
12
- timeoutSeconds: 180,
13
14
  args: [
14
15
  { name: 'text', required: true, positional: true, help: 'Prompt to send' },
15
- { name: 'timeout', required: false, help: 'Max seconds to wait (default: 60)', default: '60' },
16
+ { name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait (default: 60)', default: 60 },
16
17
  ],
17
18
  columns: ['Role', 'Text'],
18
19
  func: async (page, kwargs) => {
19
20
  const text = kwargs.text;
20
- const timeout = parseInt(kwargs.timeout, 10) || 60;
21
+ const timeout = kwargs.timeout;
22
+ if (!Number.isInteger(timeout) || timeout < 1) {
23
+ throw new ArgumentError('--timeout must be a positive integer (seconds)');
24
+ }
21
25
  const beforeTurns = await getDoubaoVisibleTurns(page);
22
26
  const beforeLines = await getDoubaoTranscriptLines(page);
23
27
  await sendDoubaoMessage(page, text);
@@ -8,6 +8,7 @@ export const detailCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [
13
14
  { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
@@ -8,6 +8,7 @@ export const historyCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [
13
14
  { name: 'limit', required: false, help: 'Max number of conversations to show', default: '50' },
@@ -8,6 +8,7 @@ export const meetingSummaryCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [
13
14
  { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
@@ -8,6 +8,7 @@ export const meetingTranscriptCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [
13
14
  { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
@@ -8,6 +8,7 @@ export const newCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [],
13
14
  columns: ['Status', 'Action'],
@@ -8,6 +8,7 @@ export const readCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [],
13
14
  columns: ['Role', 'Text'],
@@ -8,6 +8,7 @@ export const sendCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
13
14
  columns: ['Status', 'SubmittedBy', 'InjectedText'],
@@ -8,6 +8,7 @@ export const statusCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
+ browserSession: { reuse: 'site' },
11
12
  navigateBefore: false,
12
13
  args: [],
13
14
  columns: ['Status', 'Login', 'Url', 'Title'],
@@ -5,6 +5,7 @@ import { afterAll, describe, expect, it, vi } from 'vitest';
5
5
  import { wrapForEval } from '@jackwener/opencli/browser/utils';
6
6
  import { getRegistry } from '@jackwener/opencli/registry';
7
7
  import { buildCoverCheckPanelTextJs } from './draft.js';
8
+ import { createPageMock } from '../test-utils.js';
8
9
  // ─── Shared test helpers ────────────────────────────────────────────
9
10
  const tempDirs = [];
10
11
  function createTempVideo(name = 'demo.mp4') {
@@ -49,36 +50,6 @@ function createFakeTree(text, children = []) {
49
50
  }
50
51
  return node;
51
52
  }
52
- function createPageMock(evaluateResults, overrides = {}) {
53
- const evaluate = vi.fn();
54
- for (const result of evaluateResults) {
55
- evaluate.mockResolvedValueOnce(result);
56
- }
57
- return {
58
- goto: vi.fn().mockResolvedValue(undefined),
59
- evaluate,
60
- getCookies: vi.fn().mockResolvedValue([]),
61
- snapshot: vi.fn().mockResolvedValue(undefined),
62
- click: vi.fn().mockResolvedValue(undefined),
63
- typeText: vi.fn().mockResolvedValue(undefined),
64
- pressKey: vi.fn().mockResolvedValue(undefined),
65
- scrollTo: vi.fn().mockResolvedValue(undefined),
66
- getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
67
- wait: vi.fn().mockResolvedValue(undefined),
68
- tabs: vi.fn().mockResolvedValue([]),
69
- selectTab: vi.fn().mockResolvedValue(undefined),
70
- networkRequests: vi.fn().mockResolvedValue([]),
71
- consoleMessages: vi.fn().mockResolvedValue([]),
72
- scroll: vi.fn().mockResolvedValue(undefined),
73
- autoScroll: vi.fn().mockResolvedValue(undefined),
74
- installInterceptor: vi.fn().mockResolvedValue(undefined),
75
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
76
- waitForCapture: vi.fn().mockResolvedValue(undefined),
77
- screenshot: vi.fn().mockResolvedValue(''),
78
- setFileInput: vi.fn().mockResolvedValue(undefined),
79
- ...overrides,
80
- };
81
- }
82
53
  describe('douyin draft registration', () => {
83
54
  it('registers the draft command', () => {
84
55
  const registry = getRegistry();
@@ -0,0 +1,51 @@
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 './product.js';
5
+
6
+ afterEach(() => {
7
+ vi.unstubAllGlobals();
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ describe('endoflife product adapter', () => {
12
+ const cmd = getRegistry().get('endoflife/product');
13
+
14
+ it('rejects empty / malformed product before fetching', async () => {
15
+ const fetchMock = vi.fn();
16
+ vi.stubGlobal('fetch', fetchMock);
17
+
18
+ await expect(cmd.func({ product: '' })).rejects.toThrow(ArgumentError);
19
+ await expect(cmd.func({ product: 'BAD/PRODUCT' })).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('rate limited', { status: 429 })));
25
+ await expect(cmd.func({ product: 'nodejs' })).rejects.toThrow(CommandExecutionError);
26
+ });
27
+
28
+ it('maps HTTP 404 to EmptyResultError', async () => {
29
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('not found', { status: 404 })));
30
+ await expect(cmd.func({ product: 'no-such-product' })).rejects.toThrow(EmptyResultError);
31
+ });
32
+
33
+ it('normalises lts/support/eol booleans + dates and projects eolStatus', async () => {
34
+ const cycles = [
35
+ { cycle: '24', releaseDate: '2025-05-06', latest: '24.15.0', latestReleaseDate: '2026-04-15', lts: '2025-10-28', support: '2026-10-20', eol: '2028-04-30', extendedSupport: false },
36
+ { cycle: '20', releaseDate: '2023-04-18', latest: '20.20.2', latestReleaseDate: '2026-03-24', lts: '2023-10-24', support: '2024-10-22', eol: '2026-04-30', extendedSupport: true },
37
+ { cycle: 'rolling', releaseDate: '2020-01-01', latest: 'next', latestReleaseDate: '2026-01-01', lts: false, support: true, eol: false, extendedSupport: false },
38
+ ];
39
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(cycles), { status: 200 })));
40
+
41
+ const rows = await cmd.func({ product: 'nodejs' });
42
+ expect(rows).toHaveLength(3);
43
+ // Future eol -> active
44
+ expect(rows[0]).toMatchObject({ product: 'nodejs', cycle: '24', eol: '2028-04-30', eolStatus: 'active' });
45
+ expect(rows[1]).toMatchObject({ cycle: '20', eol: '2026-04-30', eolStatus: 'eol' });
46
+ // Boolean true -> "ongoing"; boolean false -> null
47
+ expect(rows[2]).toMatchObject({ cycle: 'rolling', lts: null, support: 'ongoing', eol: null, eolStatus: null });
48
+ // Round-trip: product is the slug into endoflife.date URLs
49
+ expect(rows[0].url).toBe('https://endoflife.date/nodejs');
50
+ });
51
+ });