@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,60 @@
1
+ // stackoverflow tag — list questions tagged with a given tag (most active first).
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import { ArgumentError } from '@jackwener/opencli/errors';
4
+ import {
5
+ seFetch,
6
+ normalizeLimit,
7
+ requireString,
8
+ epochToDate,
9
+ ensureItems,
10
+ decodeHtmlEntities,
11
+ } from './utils.js';
12
+
13
+ const SORT_OPTIONS = ['activity', 'votes', 'creation', 'hot', 'week', 'month'];
14
+
15
+ cli({
16
+ site: 'stackoverflow',
17
+ name: 'tag',
18
+ access: 'read',
19
+ description: 'List Stack Overflow questions tagged with a given tag (most active first).',
20
+ domain: 'stackoverflow.com',
21
+ strategy: Strategy.PUBLIC,
22
+ browser: false,
23
+ args: [
24
+ { name: 'tag', positional: true, required: true, type: 'string', help: 'Tag slug (e.g. python, rust, typescript).' },
25
+ { name: 'sort', type: 'string', default: 'activity', help: `Sort key: ${SORT_OPTIONS.join(', ')}` },
26
+ { name: 'limit', type: 'int', default: 20, help: 'Max questions to return (max 100).' },
27
+ ],
28
+ columns: ['rank', 'id', 'title', 'score', 'answers', 'views', 'isAnswered', 'tags', 'author', 'createdAt', 'lastActivityAt', 'url'],
29
+ func: async (args) => {
30
+ const tag = requireString(args.tag, 'tag').toLowerCase();
31
+ const sort = String(args.sort ?? 'activity').toLowerCase();
32
+ if (!SORT_OPTIONS.includes(sort)) {
33
+ throw new ArgumentError(`sort must be one of ${SORT_OPTIONS.join(', ')}`);
34
+ }
35
+ const limit = normalizeLimit(args.limit, 20, 100, 'limit');
36
+ const data = await seFetch(`/questions`, {
37
+ searchParams: {
38
+ tagged: tag,
39
+ order: 'desc',
40
+ sort,
41
+ pagesize: limit,
42
+ },
43
+ });
44
+ const items = ensureItems(data, `stackoverflow tag "${tag}"`);
45
+ return items.slice(0, limit).map((q, i) => ({
46
+ rank: i + 1,
47
+ id: q.question_id,
48
+ title: decodeHtmlEntities(q.title || ''),
49
+ score: q.score ?? 0,
50
+ answers: q.answer_count ?? 0,
51
+ views: q.view_count ?? 0,
52
+ isAnswered: Boolean(q.is_answered),
53
+ tags: Array.isArray(q.tags) ? q.tags.join(', ') : '',
54
+ author: decodeHtmlEntities(q.owner?.display_name || ''),
55
+ createdAt: epochToDate(q.creation_date),
56
+ lastActivityAt: epochToDate(q.last_activity_date),
57
+ url: q.link || (q.question_id ? `https://stackoverflow.com/questions/${q.question_id}` : ''),
58
+ }));
59
+ },
60
+ });
@@ -0,0 +1,50 @@
1
+ // stackoverflow user — search Stack Overflow users by name and return profiles.
2
+ import { cli, Strategy } from '@jackwener/opencli/registry';
3
+ import {
4
+ seFetch,
5
+ normalizeLimit,
6
+ requireString,
7
+ epochToDate,
8
+ ensureItems,
9
+ decodeHtmlEntities,
10
+ } from './utils.js';
11
+
12
+ cli({
13
+ site: 'stackoverflow',
14
+ name: 'user',
15
+ access: 'read',
16
+ description: 'Find Stack Overflow users by display name (highest reputation first).',
17
+ domain: 'stackoverflow.com',
18
+ strategy: Strategy.PUBLIC,
19
+ browser: false,
20
+ args: [
21
+ { name: 'name', positional: true, required: true, type: 'string', help: 'Display name (or substring) to search.' },
22
+ { name: 'limit', type: 'int', default: 10, help: 'Max users to return (max 100).' },
23
+ ],
24
+ columns: ['userId', 'displayName', 'reputation', 'goldBadges', 'silverBadges', 'bronzeBadges', 'location', 'createdAt', 'lastAccessAt', 'url'],
25
+ func: async (args) => {
26
+ const name = requireString(args.name, 'name');
27
+ const limit = normalizeLimit(args.limit, 10, 100, 'limit');
28
+ const data = await seFetch('/users', {
29
+ searchParams: {
30
+ inname: name,
31
+ order: 'desc',
32
+ sort: 'reputation',
33
+ pagesize: limit,
34
+ },
35
+ });
36
+ const items = ensureItems(data, 'stackoverflow user');
37
+ return items.slice(0, limit).map((u) => ({
38
+ userId: u.user_id,
39
+ displayName: decodeHtmlEntities(u.display_name || ''),
40
+ reputation: u.reputation ?? 0,
41
+ goldBadges: u.badge_counts?.gold ?? 0,
42
+ silverBadges: u.badge_counts?.silver ?? 0,
43
+ bronzeBadges: u.badge_counts?.bronze ?? 0,
44
+ location: decodeHtmlEntities(u.location || ''),
45
+ createdAt: epochToDate(u.creation_date),
46
+ lastAccessAt: epochToDate(u.last_access_date),
47
+ url: u.link || (u.user_id ? `https://stackoverflow.com/users/${u.user_id}` : ''),
48
+ }));
49
+ },
50
+ });
@@ -0,0 +1,118 @@
1
+ // Shared helpers for stackoverflow adapters using the Stack Exchange API.
2
+ //
3
+ // Public endpoint (api.stackexchange.com 2.3) accepts unauthenticated traffic
4
+ // up to 300 requests/day per IP for read endpoints, plenty for ad-hoc CLI use.
5
+ // We always set `site=stackoverflow` and decode the gzipped/HTML body via the
6
+ // returned JSON envelope.
7
+ import {
8
+ ArgumentError,
9
+ CommandExecutionError,
10
+ EmptyResultError,
11
+ } from '@jackwener/opencli/errors';
12
+
13
+ export const SE_API = 'https://api.stackexchange.com/2.3';
14
+ export const SE_SITE = 'stackoverflow';
15
+
16
+ const UA = 'opencli-stackoverflow (+https://github.com/jackwener/opencli)';
17
+
18
+ /** Validate `limit` per typed-fail-fast convention (no silent clamp). */
19
+ export function normalizeLimit(value, defaultValue, maxValue, label = 'limit') {
20
+ const raw = value ?? defaultValue;
21
+ const limit = Number(raw);
22
+ if (!Number.isInteger(limit) || limit <= 0) {
23
+ throw new ArgumentError(`${label} must be a positive integer`);
24
+ }
25
+ if (limit > maxValue) {
26
+ throw new ArgumentError(`${label} must be <= ${maxValue}`);
27
+ }
28
+ return limit;
29
+ }
30
+
31
+ export function requireString(value, label) {
32
+ const raw = String(value ?? '').trim();
33
+ if (!raw) {
34
+ throw new ArgumentError(`${label} cannot be empty`);
35
+ }
36
+ return raw;
37
+ }
38
+
39
+ /** Fetch a Stack Exchange API endpoint and return parsed JSON envelope. */
40
+ export async function seFetch(path, { searchParams } = {}) {
41
+ const url = new URL(path.startsWith('http') ? path : `${SE_API}${path.startsWith('/') ? '' : '/'}${path}`);
42
+ if (searchParams) {
43
+ for (const [k, v] of Object.entries(searchParams)) {
44
+ if (v == null || v === '') continue;
45
+ url.searchParams.set(k, String(v));
46
+ }
47
+ }
48
+ if (!url.searchParams.has('site')) url.searchParams.set('site', SE_SITE);
49
+
50
+ let resp;
51
+ try {
52
+ resp = await fetch(url, {
53
+ headers: {
54
+ 'Accept': 'application/json',
55
+ 'Accept-Encoding': 'gzip',
56
+ 'User-Agent': UA,
57
+ },
58
+ });
59
+ } catch (error) {
60
+ throw new CommandExecutionError(`stack exchange request failed: ${error?.message || error}`);
61
+ }
62
+ if (resp.status === 429) {
63
+ throw new CommandExecutionError('stack exchange returned HTTP 429 (rate limited)', 'Wait a few seconds and retry, or lower --limit.');
64
+ }
65
+ if (!resp.ok) {
66
+ let body = '';
67
+ try { body = (await resp.json())?.error_message || ''; } catch { /* ignore */ }
68
+ throw new CommandExecutionError(`stack exchange HTTP ${resp.status}: ${body || resp.statusText}`);
69
+ }
70
+ let data;
71
+ try {
72
+ data = await resp.json();
73
+ } catch (error) {
74
+ throw new CommandExecutionError(`stack exchange returned malformed JSON: ${error?.message || error}`);
75
+ }
76
+ if (data?.error_id) {
77
+ throw new CommandExecutionError(
78
+ `stack exchange API error: ${data.error_message || data.error_name}`,
79
+ 'Inspect the URL in a browser for the canonical error context.',
80
+ );
81
+ }
82
+ return data;
83
+ }
84
+
85
+ /** Convert SE epoch seconds to YYYY-MM-DD. */
86
+ export function epochToDate(value) {
87
+ if (value == null || value === '') return '';
88
+ const n = Number(value);
89
+ if (!Number.isFinite(n) || n <= 0) return '';
90
+ return new Date(n * 1000).toISOString().slice(0, 10);
91
+ }
92
+
93
+ /** Throw EmptyResultError when an /items array is empty. */
94
+ export function ensureItems(data, label) {
95
+ const items = Array.isArray(data?.items) ? data.items : [];
96
+ if (items.length === 0) {
97
+ throw new EmptyResultError(label, `${label} returned no items.`);
98
+ }
99
+ return items;
100
+ }
101
+
102
+ const HTML_ENTITY_MAP = {
103
+ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"',
104
+ '&#39;': "'", '&apos;': "'", '&nbsp;': ' ',
105
+ };
106
+
107
+ /**
108
+ * Decode the small set of HTML entities Stack Exchange emits in display
109
+ * names and titles (e.g. "Jon Skeet&#39;s mentor"). Decimal/hex numeric
110
+ * refs are also handled.
111
+ */
112
+ export function decodeHtmlEntities(value) {
113
+ if (value == null) return '';
114
+ return String(value)
115
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
116
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
117
+ .replace(/&(amp|lt|gt|quot|#39|apos|nbsp);/g, (m) => HTML_ENTITY_MAP[m] || m);
118
+ }
@@ -0,0 +1,67 @@
1
+ // steam app — fetch a single Steam game / DLC / app's storefront detail.
2
+ //
3
+ // Hits the public appdetails API (`/api/appdetails?appids=<id>`). Surfaces
4
+ // the columns most useful for an agent: name, type, free flag, release
5
+ // date, developers / publishers, price, metacritic, recommendations,
6
+ // genres / categories joined.
7
+ import { cli, Strategy } from '@jackwener/opencli/registry';
8
+ import { EmptyResultError } from '@jackwener/opencli/errors';
9
+ import { STEAM_STORE, decodeHtmlEntities, priceCents, requireAppId, requireCountryCode, steamFetch } from './utils.js';
10
+
11
+ function joinNames(list) {
12
+ if (!Array.isArray(list)) return '';
13
+ return list
14
+ .map((entry) => (entry && typeof entry === 'object' ? entry.description ?? entry.name ?? '' : String(entry)))
15
+ .filter(Boolean)
16
+ .join(', ');
17
+ }
18
+
19
+ cli({
20
+ site: 'steam',
21
+ name: 'app',
22
+ access: 'read',
23
+ description: 'Steam storefront detail for a single app id',
24
+ domain: 'store.steampowered.com',
25
+ strategy: Strategy.PUBLIC,
26
+ browser: false,
27
+ args: [
28
+ { name: 'id', positional: true, required: true, help: 'Numeric Steam app id (e.g. "620" for Portal 2)' },
29
+ { name: 'currency', default: 'us', help: 'Storefront country code (e.g. us / cn / jp / de)' },
30
+ ],
31
+ columns: [
32
+ 'id', 'name', 'type', 'isFree', 'releaseDate', 'developers', 'publishers',
33
+ 'price', 'currency', 'metacritic', 'recommendations', 'genres', 'categories',
34
+ 'shortDescription', 'website', 'url',
35
+ ],
36
+ func: async (args) => {
37
+ const appId = requireAppId(args.id);
38
+ const cc = requireCountryCode(args.currency);
39
+ const url = `${STEAM_STORE}/api/appdetails?appids=${appId}&l=en&cc=${encodeURIComponent(cc)}`;
40
+ const body = await steamFetch(url, `steam app ${appId}`);
41
+ const wrapper = body?.[appId];
42
+ if (!wrapper || wrapper.success !== true || !wrapper.data) {
43
+ throw new EmptyResultError('steam app', `Steam app id ${appId} returned no data (may be region-locked or removed).`);
44
+ }
45
+ const data = wrapper.data;
46
+ const isFree = data.is_free === true;
47
+ const priceFinal = priceCents(data?.price_overview?.final ?? null);
48
+ return [{
49
+ id: String(data.steam_appid ?? appId),
50
+ name: decodeHtmlEntities(data.name ?? ''),
51
+ type: String(data.type ?? ''),
52
+ isFree,
53
+ releaseDate: String(data?.release_date?.date ?? ''),
54
+ developers: Array.isArray(data.developers) ? data.developers.join(', ') : '',
55
+ publishers: Array.isArray(data.publishers) ? data.publishers.join(', ') : '',
56
+ price: isFree ? 0 : priceFinal,
57
+ currency: String(data?.price_overview?.currency ?? '').toUpperCase(),
58
+ metacritic: data?.metacritic?.score != null ? Number(data.metacritic.score) : null,
59
+ recommendations: data?.recommendations?.total != null ? Number(data.recommendations.total) : null,
60
+ genres: joinNames(data.genres),
61
+ categories: joinNames(data.categories),
62
+ shortDescription: decodeHtmlEntities(data.short_description ?? ''),
63
+ website: String(data.website ?? ''),
64
+ url: `${STEAM_STORE}/app/${data.steam_appid ?? appId}/`,
65
+ }];
66
+ },
67
+ });
@@ -0,0 +1,58 @@
1
+ // steam search — search the Steam storefront catalog.
2
+ //
3
+ // Hits the public storesearch API (`/api/storesearch/?term=…`). Returns
4
+ // matched apps with id / name / price / metascore / platform support so
5
+ // the row's `id` round-trips into `steam app`.
6
+ import { cli, Strategy } from '@jackwener/opencli/registry';
7
+ import { EmptyResultError } from '@jackwener/opencli/errors';
8
+ import {
9
+ STEAM_STORE,
10
+ decodeHtmlEntities,
11
+ priceCents,
12
+ requireBoundedInt,
13
+ requireCountryCode,
14
+ requireString,
15
+ steamFetch,
16
+ } from './utils.js';
17
+
18
+ function platformList(platforms) {
19
+ if (!platforms || typeof platforms !== 'object') return '';
20
+ return ['windows', 'mac', 'linux'].filter((p) => platforms[p]).join(',');
21
+ }
22
+
23
+ cli({
24
+ site: 'steam',
25
+ name: 'search',
26
+ access: 'read',
27
+ description: 'Search the Steam storefront by name keyword',
28
+ domain: 'store.steampowered.com',
29
+ strategy: Strategy.PUBLIC,
30
+ browser: false,
31
+ args: [
32
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "portal", "stardew")' },
33
+ { name: 'limit', type: 'int', default: 20, help: 'Max results (1-50)' },
34
+ { name: 'currency', default: 'us', help: 'Storefront country code (e.g. us / cn / jp / de)' },
35
+ ],
36
+ columns: ['rank', 'id', 'name', 'price', 'currency', 'metascore', 'platforms', 'url'],
37
+ func: async (args) => {
38
+ const query = requireString(args.query, 'query');
39
+ const limit = requireBoundedInt(args.limit, 20, 50);
40
+ const cc = requireCountryCode(args.currency);
41
+ const url = `${STEAM_STORE}/api/storesearch/?term=${encodeURIComponent(query)}&l=en&cc=${encodeURIComponent(cc)}`;
42
+ const body = await steamFetch(url, 'steam search');
43
+ const items = Array.isArray(body?.items) ? body.items : [];
44
+ if (!items.length) {
45
+ throw new EmptyResultError('steam search', `No Steam results matched "${query}".`);
46
+ }
47
+ return items.slice(0, limit).map((item, i) => ({
48
+ rank: i + 1,
49
+ id: String(item.id ?? ''),
50
+ name: decodeHtmlEntities(item.name ?? ''),
51
+ price: priceCents(item?.price?.final ?? null),
52
+ currency: String(item?.price?.currency ?? '').toUpperCase(),
53
+ metascore: item.metascore != null && item.metascore !== '' ? Number(item.metascore) : null,
54
+ platforms: platformList(item.platforms),
55
+ url: item.id ? `${STEAM_STORE}/app/${item.id}/` : '',
56
+ }));
57
+ },
58
+ });
@@ -0,0 +1,46 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import { ArgumentError } from '@jackwener/opencli/errors';
4
+ import { decodeHtmlEntities, requireCountryCode } from './utils.js';
5
+ import './search.js';
6
+ import './app.js';
7
+
8
+ afterEach(() => {
9
+ vi.unstubAllGlobals();
10
+ });
11
+
12
+ describe('steam adapter helpers', () => {
13
+ it('decodes Steam HTML entities from API strings', () => {
14
+ expect(decodeHtmlEntities('&quot;Perpetual Testing Initiative&quot; &amp; Co-op')).toBe('"Perpetual Testing Initiative" & Co-op');
15
+ expect(decodeHtmlEntities('Portal &#x32;')).toBe('Portal 2');
16
+ });
17
+
18
+ it('validates storefront country code without silent fallback', () => {
19
+ expect(requireCountryCode(undefined)).toBe('us');
20
+ expect(requireCountryCode(' CN ')).toBe('cn');
21
+ expect(() => requireCountryCode('')).toThrow(ArgumentError);
22
+ expect(() => requireCountryCode('usd')).toThrow(ArgumentError);
23
+ expect(() => requireCountryCode('$$')).toThrow(ArgumentError);
24
+ });
25
+ });
26
+
27
+ describe('steam command argument contracts', () => {
28
+ it('rejects invalid search currency before fetching', async () => {
29
+ const fetchMock = vi.fn();
30
+ vi.stubGlobal('fetch', fetchMock);
31
+
32
+ const search = getRegistry().get('steam/search');
33
+ await expect(search.func({ query: 'portal', limit: 5, currency: '$$$' })).rejects.toThrow(ArgumentError);
34
+ await expect(search.func({ query: 'portal', limit: 5, currency: '' })).rejects.toThrow(ArgumentError);
35
+ expect(fetchMock).not.toHaveBeenCalled();
36
+ });
37
+
38
+ it('rejects invalid app currency before fetching', async () => {
39
+ const fetchMock = vi.fn();
40
+ vi.stubGlobal('fetch', fetchMock);
41
+
42
+ const app = getRegistry().get('steam/app');
43
+ await expect(app.func({ id: '620', currency: 'usd' })).rejects.toThrow(ArgumentError);
44
+ expect(fetchMock).not.toHaveBeenCalled();
45
+ });
46
+ });
@@ -0,0 +1,107 @@
1
+ // Shared helpers for the steam adapters that hit Steam's storefront JSON
2
+ // endpoints (no browser).
3
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
4
+
5
+ export const STEAM_STORE = 'https://store.steampowered.com';
6
+ const UA = 'opencli-steam-adapter (+https://github.com/jackwener/opencli)';
7
+
8
+ export function requireString(value, label) {
9
+ const s = String(value ?? '').trim();
10
+ if (!s) {
11
+ throw new ArgumentError(`steam ${label} cannot be empty`);
12
+ }
13
+ return s;
14
+ }
15
+
16
+ export function requireCountryCode(value, defaultValue = 'us') {
17
+ const raw = value === undefined || value === null ? defaultValue : value;
18
+ const code = String(raw).trim().toLowerCase();
19
+ if (!/^[a-z]{2}$/.test(code)) {
20
+ throw new ArgumentError(
21
+ `steam currency must be a two-letter storefront country code (got "${value}")`,
22
+ 'Examples: us, cn, jp, de. This controls Steam regional pricing and availability.',
23
+ );
24
+ }
25
+ return code;
26
+ }
27
+
28
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
29
+ const raw = value ?? defaultValue;
30
+ const n = typeof raw === 'number' ? raw : Number(raw);
31
+ if (!Number.isInteger(n) || n <= 0) {
32
+ throw new ArgumentError(`steam ${label} must be a positive integer`);
33
+ }
34
+ if (n > maxValue) {
35
+ throw new ArgumentError(`steam ${label} must be <= ${maxValue}`);
36
+ }
37
+ return n;
38
+ }
39
+
40
+ export function requireAppId(value) {
41
+ const s = String(value ?? '').trim();
42
+ if (!s) {
43
+ throw new ArgumentError('steam app id is required (e.g. "620" for Portal 2)');
44
+ }
45
+ if (!/^\d+$/.test(s)) {
46
+ throw new ArgumentError(
47
+ `steam app id "${value}" must be a positive integer`,
48
+ 'Copy the numeric id from `steam search` or the URL `store.steampowered.com/app/<id>/`.',
49
+ );
50
+ }
51
+ return s;
52
+ }
53
+
54
+ export async function steamFetch(url, label) {
55
+ let resp;
56
+ try {
57
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
58
+ }
59
+ catch (err) {
60
+ throw new CommandExecutionError(
61
+ `${label} request failed: ${err?.message ?? err}`,
62
+ 'Check that store.steampowered.com is reachable from this network.',
63
+ );
64
+ }
65
+ if (resp.status === 429) {
66
+ throw new CommandExecutionError(
67
+ `${label} returned HTTP 429 (rate limited)`,
68
+ 'Steam throttles bursty traffic; wait a few seconds and retry.',
69
+ );
70
+ }
71
+ if (resp.status === 404) {
72
+ throw new EmptyResultError(label, 'Steam returned 404 — the resource does not exist.');
73
+ }
74
+ if (!resp.ok) {
75
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
76
+ }
77
+ let body;
78
+ try {
79
+ body = await resp.json();
80
+ }
81
+ catch (err) {
82
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
83
+ }
84
+ return body;
85
+ }
86
+
87
+ export function asString(value) {
88
+ return value == null ? '' : String(value);
89
+ }
90
+
91
+ const HTML_ENTITIES = {
92
+ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'", '&#39;': "'", '&nbsp;': ' ',
93
+ };
94
+
95
+ export function decodeHtmlEntities(value) {
96
+ return String(value ?? '')
97
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
98
+ .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
99
+ .replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g, (m) => HTML_ENTITIES[m] || m);
100
+ }
101
+
102
+ export function priceCents(cents) {
103
+ if (cents == null || cents === '') return null;
104
+ const n = Number(cents);
105
+ if (!Number.isFinite(n)) return null;
106
+ return Number((n / 100).toFixed(2));
107
+ }
@@ -5,30 +5,7 @@ import './detail.js';
5
5
  import './reviews.js';
6
6
  import './cart.js';
7
7
  import './add-cart.js';
8
- function createPageMock() {
9
- return {
10
- goto: vi.fn().mockResolvedValue(undefined),
11
- evaluate: vi.fn().mockResolvedValue({ title: 'Demo', price: '¥99' }),
12
- snapshot: vi.fn().mockResolvedValue(undefined),
13
- click: vi.fn().mockResolvedValue(undefined),
14
- typeText: vi.fn().mockResolvedValue(undefined),
15
- pressKey: vi.fn().mockResolvedValue(undefined),
16
- scrollTo: vi.fn().mockResolvedValue(undefined),
17
- getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
18
- wait: vi.fn().mockResolvedValue(undefined),
19
- tabs: vi.fn().mockResolvedValue([]),
20
- selectTab: vi.fn().mockResolvedValue(undefined),
21
- networkRequests: vi.fn().mockResolvedValue([]),
22
- consoleMessages: vi.fn().mockResolvedValue([]),
23
- scroll: vi.fn().mockResolvedValue(undefined),
24
- autoScroll: vi.fn().mockResolvedValue(undefined),
25
- installInterceptor: vi.fn().mockResolvedValue(undefined),
26
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
27
- getCookies: vi.fn().mockResolvedValue([]),
28
- screenshot: vi.fn().mockResolvedValue(''),
29
- waitForCapture: vi.fn().mockResolvedValue(undefined),
30
- };
31
- }
8
+ import { createPageMock } from '../test-utils.js';
32
9
  describe('taobao command registration', () => {
33
10
  it('registers all taobao shopping commands', () => {
34
11
  for (const name of ['search', 'detail', 'reviews', 'cart', 'add-cart']) {
@@ -0,0 +1,61 @@
1
+ import { vi } from 'vitest';
2
+
3
+ /**
4
+ * Create a page mock with all standard browser automation methods.
5
+ *
6
+ * @param {any[]} evaluateResults - Sequential results for page.evaluate() calls
7
+ * @param {Record<string, any>} [overrides] - Override or add mock methods
8
+ * @returns A mock page object compatible with OpenCLI's browser page interface
9
+ */
10
+ export function createPageMock(evaluateResults = [], overrides = {}) {
11
+ const evaluate = vi.fn();
12
+ for (const result of evaluateResults) {
13
+ evaluate.mockResolvedValueOnce(result);
14
+ }
15
+ return {
16
+ // Navigation
17
+ goto: vi.fn().mockResolvedValue(undefined),
18
+ tabs: vi.fn().mockResolvedValue([]),
19
+ selectTab: vi.fn().mockResolvedValue(undefined),
20
+ closeTab: vi.fn().mockResolvedValue(undefined),
21
+ newTab: vi.fn().mockResolvedValue(undefined),
22
+
23
+ // Content extraction
24
+ evaluate,
25
+ snapshot: vi.fn().mockResolvedValue(undefined),
26
+ screenshot: vi.fn().mockResolvedValue(''),
27
+
28
+ // User interaction
29
+ click: vi.fn().mockResolvedValue(undefined),
30
+ typeText: vi.fn().mockResolvedValue(undefined),
31
+ pressKey: vi.fn().mockResolvedValue(undefined),
32
+ scrollTo: vi.fn().mockResolvedValue(undefined),
33
+ scroll: vi.fn().mockResolvedValue(undefined),
34
+ autoScroll: vi.fn().mockResolvedValue(undefined),
35
+ setFileInput: vi.fn().mockResolvedValue(undefined),
36
+
37
+ // Form handling
38
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
39
+
40
+ // Monitoring
41
+ networkRequests: vi.fn().mockResolvedValue([]),
42
+ consoleMessages: vi.fn().mockResolvedValue([]),
43
+
44
+ // Request interception
45
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
46
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
47
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
48
+
49
+ // Network capture
50
+ startNetworkCapture: vi.fn().mockResolvedValue(undefined),
51
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
52
+
53
+ // Auth
54
+ getCookies: vi.fn().mockResolvedValue([]),
55
+
56
+ // Wait
57
+ wait: vi.fn().mockResolvedValue(undefined),
58
+
59
+ ...overrides,
60
+ };
61
+ }
package/clis/tieba/hot.js CHANGED
@@ -9,7 +9,6 @@ cli({
9
9
  domain: 'tieba.baidu.com',
10
10
  strategy: Strategy.PUBLIC,
11
11
  browser: true,
12
- navigateBefore: false,
13
12
  args: [
14
13
  { name: 'limit', type: 'int', default: 20, help: 'Number of items to return' },
15
14
  ],