@jackwener/opencli 1.4.0 → 1.5.0

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 (514) hide show
  1. package/.github/actions/setup-chrome/action.yml +5 -4
  2. package/.github/workflows/build-extension.yml +2 -6
  3. package/.github/workflows/ci.yml +37 -3
  4. package/.github/workflows/e2e-headed.yml +16 -3
  5. package/CHANGELOG.md +23 -0
  6. package/PRIVACY.md +57 -0
  7. package/README.md +36 -7
  8. package/README.zh-CN.md +13 -6
  9. package/SKILL.md +103 -2
  10. package/dist/browser/cdp.d.ts +2 -1
  11. package/dist/browser/discover.d.ts +4 -1
  12. package/dist/browser/discover.js +6 -2
  13. package/dist/browser/errors.d.ts +2 -2
  14. package/dist/browser/errors.js +4 -12
  15. package/dist/browser/mcp.d.ts +2 -1
  16. package/dist/build-manifest.d.ts +2 -0
  17. package/dist/build-manifest.js +39 -14
  18. package/dist/build-manifest.test.js +21 -0
  19. package/dist/capabilityRouting.d.ts +2 -0
  20. package/dist/capabilityRouting.js +2 -1
  21. package/dist/cli-manifest.json +1838 -151
  22. package/dist/cli.js +34 -3
  23. package/dist/clis/36kr/article.d.ts +1 -0
  24. package/dist/clis/36kr/article.js +62 -0
  25. package/dist/clis/36kr/hot.d.ts +3 -0
  26. package/dist/clis/36kr/hot.js +80 -0
  27. package/dist/clis/36kr/hot.test.d.ts +1 -0
  28. package/dist/clis/36kr/hot.test.js +15 -0
  29. package/dist/clis/36kr/news.d.ts +1 -0
  30. package/dist/clis/36kr/news.js +51 -0
  31. package/dist/clis/36kr/news.test.d.ts +1 -0
  32. package/dist/clis/36kr/news.test.js +85 -0
  33. package/dist/clis/36kr/search.d.ts +1 -0
  34. package/dist/clis/36kr/search.js +72 -0
  35. package/dist/clis/apple-podcasts/search.js +2 -1
  36. package/dist/clis/arxiv/search.js +2 -2
  37. package/dist/clis/bbc/news.js +0 -1
  38. package/dist/clis/bilibili/comments.d.ts +5 -0
  39. package/dist/clis/bilibili/comments.js +40 -0
  40. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  41. package/dist/clis/bilibili/comments.test.js +82 -0
  42. package/dist/clis/chatgpt/ask.js +29 -14
  43. package/dist/clis/chatgpt/ax.d.ts +6 -0
  44. package/dist/clis/chatgpt/ax.js +172 -1
  45. package/dist/clis/chatgpt/model.d.ts +1 -0
  46. package/dist/clis/chatgpt/model.js +24 -0
  47. package/dist/clis/chatgpt/send.js +12 -3
  48. package/dist/clis/ctrip/search.js +0 -1
  49. package/dist/clis/douban/download.d.ts +1 -0
  50. package/dist/clis/douban/download.js +67 -0
  51. package/dist/clis/douban/download.test.d.ts +1 -0
  52. package/dist/clis/douban/download.test.js +170 -0
  53. package/dist/clis/douban/photos.d.ts +1 -0
  54. package/dist/clis/douban/photos.js +34 -0
  55. package/dist/clis/douban/utils.d.ts +25 -0
  56. package/dist/clis/douban/utils.js +190 -1
  57. package/dist/clis/douban/utils.test.d.ts +1 -0
  58. package/dist/clis/douban/utils.test.js +64 -0
  59. package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
  60. package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
  61. package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
  62. package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
  63. package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
  64. package/dist/clis/douyin/_shared/creation-id.js +5 -0
  65. package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
  66. package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
  67. package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
  68. package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
  69. package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
  70. package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
  71. package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
  72. package/dist/clis/douyin/_shared/sts2.js +15 -0
  73. package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
  74. package/dist/clis/douyin/_shared/text-extra.js +15 -0
  75. package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
  76. package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
  77. package/dist/clis/douyin/_shared/timing.d.ts +2 -0
  78. package/dist/clis/douyin/_shared/timing.js +22 -0
  79. package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
  80. package/dist/clis/douyin/_shared/timing.test.js +28 -0
  81. package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
  82. package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
  83. package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
  84. package/dist/clis/douyin/_shared/tos-upload.js +295 -0
  85. package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
  86. package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
  87. package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
  88. package/dist/clis/douyin/_shared/transcode.js +45 -0
  89. package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
  90. package/dist/clis/douyin/_shared/transcode.test.js +93 -0
  91. package/dist/clis/douyin/_shared/types.d.ts +26 -0
  92. package/dist/clis/douyin/_shared/types.js +1 -0
  93. package/dist/clis/douyin/activities.d.ts +1 -0
  94. package/dist/clis/douyin/activities.js +20 -0
  95. package/dist/clis/douyin/activities.test.d.ts +1 -0
  96. package/dist/clis/douyin/activities.test.js +22 -0
  97. package/dist/clis/douyin/collections.d.ts +1 -0
  98. package/dist/clis/douyin/collections.js +22 -0
  99. package/dist/clis/douyin/collections.test.d.ts +1 -0
  100. package/dist/clis/douyin/collections.test.js +23 -0
  101. package/dist/clis/douyin/delete.d.ts +1 -0
  102. package/dist/clis/douyin/delete.js +18 -0
  103. package/dist/clis/douyin/delete.test.d.ts +1 -0
  104. package/dist/clis/douyin/delete.test.js +11 -0
  105. package/dist/clis/douyin/draft.d.ts +14 -0
  106. package/dist/clis/douyin/draft.js +237 -0
  107. package/dist/clis/douyin/draft.test.d.ts +1 -0
  108. package/dist/clis/douyin/draft.test.js +11 -0
  109. package/dist/clis/douyin/drafts.d.ts +1 -0
  110. package/dist/clis/douyin/drafts.js +23 -0
  111. package/dist/clis/douyin/drafts.test.d.ts +1 -0
  112. package/dist/clis/douyin/drafts.test.js +11 -0
  113. package/dist/clis/douyin/hashtag.d.ts +1 -0
  114. package/dist/clis/douyin/hashtag.js +45 -0
  115. package/dist/clis/douyin/hashtag.test.d.ts +1 -0
  116. package/dist/clis/douyin/hashtag.test.js +25 -0
  117. package/dist/clis/douyin/location.d.ts +1 -0
  118. package/dist/clis/douyin/location.js +24 -0
  119. package/dist/clis/douyin/location.test.d.ts +1 -0
  120. package/dist/clis/douyin/location.test.js +23 -0
  121. package/dist/clis/douyin/profile.d.ts +1 -0
  122. package/dist/clis/douyin/profile.js +28 -0
  123. package/dist/clis/douyin/profile.test.d.ts +1 -0
  124. package/dist/clis/douyin/profile.test.js +11 -0
  125. package/dist/clis/douyin/publish.d.ts +14 -0
  126. package/dist/clis/douyin/publish.js +288 -0
  127. package/dist/clis/douyin/publish.test.d.ts +1 -0
  128. package/dist/clis/douyin/publish.test.js +38 -0
  129. package/dist/clis/douyin/stats.d.ts +1 -0
  130. package/dist/clis/douyin/stats.js +27 -0
  131. package/dist/clis/douyin/stats.test.d.ts +1 -0
  132. package/dist/clis/douyin/stats.test.js +22 -0
  133. package/dist/clis/douyin/update.d.ts +1 -0
  134. package/dist/clis/douyin/update.js +31 -0
  135. package/dist/clis/douyin/update.test.d.ts +1 -0
  136. package/dist/clis/douyin/update.test.js +11 -0
  137. package/dist/clis/douyin/videos.d.ts +1 -0
  138. package/dist/clis/douyin/videos.js +34 -0
  139. package/dist/clis/douyin/videos.test.d.ts +1 -0
  140. package/dist/clis/douyin/videos.test.js +11 -0
  141. package/dist/clis/hackernews/search.yaml +1 -1
  142. package/dist/clis/imdb/person.d.ts +1 -0
  143. package/dist/clis/imdb/person.js +203 -0
  144. package/dist/clis/imdb/reviews.d.ts +1 -0
  145. package/dist/clis/imdb/reviews.js +88 -0
  146. package/dist/clis/imdb/search.d.ts +1 -0
  147. package/dist/clis/imdb/search.js +161 -0
  148. package/dist/clis/imdb/title.d.ts +1 -0
  149. package/dist/clis/imdb/title.js +93 -0
  150. package/dist/clis/imdb/top.d.ts +1 -0
  151. package/dist/clis/imdb/top.js +53 -0
  152. package/dist/clis/imdb/trending.d.ts +1 -0
  153. package/dist/clis/imdb/trending.js +52 -0
  154. package/dist/clis/imdb/utils.d.ts +46 -0
  155. package/dist/clis/imdb/utils.js +285 -0
  156. package/dist/clis/imdb/utils.test.d.ts +1 -0
  157. package/dist/clis/imdb/utils.test.js +88 -0
  158. package/dist/clis/instagram/search.yaml +2 -1
  159. package/dist/clis/jd/item.d.ts +4 -0
  160. package/dist/clis/jd/item.js +16 -15
  161. package/dist/clis/jd/item.test.js +16 -1
  162. package/dist/clis/linux-do/categories.yaml +38 -9
  163. package/dist/clis/linux-do/category.d.ts +1 -0
  164. package/dist/clis/linux-do/category.js +36 -0
  165. package/dist/clis/linux-do/feed.d.ts +45 -0
  166. package/dist/clis/linux-do/feed.js +397 -0
  167. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  168. package/dist/clis/linux-do/feed.test.js +118 -0
  169. package/dist/clis/linux-do/hot.d.ts +1 -0
  170. package/dist/clis/linux-do/hot.js +25 -0
  171. package/dist/clis/linux-do/latest.d.ts +1 -0
  172. package/dist/clis/linux-do/latest.js +18 -0
  173. package/dist/clis/linux-do/search.yaml +3 -1
  174. package/dist/clis/linux-do/tags.yaml +41 -0
  175. package/dist/clis/linux-do/topic.yaml +41 -3
  176. package/dist/clis/linux-do/user-posts.yaml +67 -0
  177. package/dist/clis/linux-do/user-topics.yaml +54 -0
  178. package/dist/clis/medium/search.js +1 -1
  179. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  180. package/dist/clis/paperreview/commands.test.js +243 -0
  181. package/dist/clis/paperreview/feedback.d.ts +1 -0
  182. package/dist/clis/paperreview/feedback.js +52 -0
  183. package/dist/clis/paperreview/review.d.ts +1 -0
  184. package/dist/clis/paperreview/review.js +37 -0
  185. package/dist/clis/paperreview/submit.d.ts +1 -0
  186. package/dist/clis/paperreview/submit.js +85 -0
  187. package/dist/clis/paperreview/utils.d.ts +46 -0
  188. package/dist/clis/paperreview/utils.js +197 -0
  189. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  190. package/dist/clis/paperreview/utils.test.js +49 -0
  191. package/dist/clis/producthunt/browse.d.ts +1 -0
  192. package/dist/clis/producthunt/browse.js +99 -0
  193. package/dist/clis/producthunt/hot.d.ts +1 -0
  194. package/dist/clis/producthunt/hot.js +110 -0
  195. package/dist/clis/producthunt/posts.d.ts +1 -0
  196. package/dist/clis/producthunt/posts.js +28 -0
  197. package/dist/clis/producthunt/today.d.ts +1 -0
  198. package/dist/clis/producthunt/today.js +35 -0
  199. package/dist/clis/producthunt/utils.d.ts +29 -0
  200. package/dist/clis/producthunt/utils.js +99 -0
  201. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  202. package/dist/clis/producthunt/utils.test.js +64 -0
  203. package/dist/clis/reuters/search.js +0 -1
  204. package/dist/clis/twitter/article.js +4 -28
  205. package/dist/clis/twitter/likes.d.ts +24 -0
  206. package/dist/clis/twitter/likes.js +217 -0
  207. package/dist/clis/twitter/likes.test.d.ts +1 -0
  208. package/dist/clis/twitter/likes.test.js +85 -0
  209. package/dist/clis/twitter/profile.js +4 -28
  210. package/dist/clis/twitter/search.js +7 -4
  211. package/dist/clis/twitter/search.test.js +56 -2
  212. package/dist/clis/twitter/shared.d.ts +6 -0
  213. package/dist/clis/twitter/shared.js +35 -0
  214. package/dist/clis/twitter/timeline.js +2 -13
  215. package/dist/clis/weibo/comments.d.ts +1 -0
  216. package/dist/clis/weibo/comments.js +53 -0
  217. package/dist/clis/weibo/feed.d.ts +1 -0
  218. package/dist/clis/weibo/feed.js +56 -0
  219. package/dist/clis/weibo/hot.js +0 -1
  220. package/dist/clis/weibo/me.d.ts +1 -0
  221. package/dist/clis/weibo/me.js +76 -0
  222. package/dist/clis/weibo/post.d.ts +1 -0
  223. package/dist/clis/weibo/post.js +75 -0
  224. package/dist/clis/weibo/user.d.ts +1 -0
  225. package/dist/clis/weibo/user.js +63 -0
  226. package/dist/clis/weibo/utils.d.ts +6 -0
  227. package/dist/clis/weibo/utils.js +30 -0
  228. package/dist/clis/weixin/download.d.ts +17 -0
  229. package/dist/clis/weixin/download.js +88 -20
  230. package/dist/clis/weread/book.js +2 -2
  231. package/dist/clis/weread/commands.test.d.ts +3 -0
  232. package/dist/clis/weread/commands.test.js +43 -0
  233. package/dist/clis/weread/highlights.js +2 -2
  234. package/dist/clis/weread/notebooks.js +2 -2
  235. package/dist/clis/weread/notes.js +3 -3
  236. package/dist/clis/weread/search.js +3 -2
  237. package/dist/clis/weread/shelf.js +2 -2
  238. package/dist/clis/weread/utils.d.ts +4 -4
  239. package/dist/clis/weread/utils.js +32 -14
  240. package/dist/clis/weread/utils.test.js +1 -28
  241. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  242. package/dist/clis/xiaohongshu/comments.js +74 -0
  243. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  244. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  245. package/dist/clis/xiaohongshu/publish.js +114 -18
  246. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  247. package/dist/clis/xiaohongshu/publish.test.js +119 -0
  248. package/dist/clis/xueqiu/search.yaml +2 -1
  249. package/dist/clis/yahoo-finance/quote.js +0 -1
  250. package/dist/clis/youtube/channel.d.ts +1 -0
  251. package/dist/clis/youtube/channel.js +150 -0
  252. package/dist/clis/youtube/comments.d.ts +1 -0
  253. package/dist/clis/youtube/comments.js +95 -0
  254. package/dist/clis/youtube/search.js +0 -1
  255. package/dist/clis/zhihu/search.yaml +2 -1
  256. package/dist/commanderAdapter.d.ts +1 -0
  257. package/dist/commanderAdapter.js +176 -29
  258. package/dist/commanderAdapter.test.d.ts +1 -0
  259. package/dist/commanderAdapter.test.js +62 -0
  260. package/dist/daemon.js +17 -1
  261. package/dist/discovery.js +8 -14
  262. package/dist/doctor.d.ts +1 -0
  263. package/dist/doctor.js +9 -2
  264. package/dist/download/index.js +63 -51
  265. package/dist/download/index.test.js +17 -4
  266. package/dist/errors.d.ts +3 -1
  267. package/dist/errors.js +15 -32
  268. package/dist/execution.d.ts +1 -3
  269. package/dist/execution.js +21 -1
  270. package/dist/external-clis.yaml +0 -17
  271. package/dist/hooks.js +2 -0
  272. package/dist/main.js +5 -0
  273. package/dist/output.js +5 -1
  274. package/dist/pipeline/executor.js +3 -4
  275. package/dist/plugin-manifest.d.ts +70 -0
  276. package/dist/plugin-manifest.js +160 -0
  277. package/dist/plugin-manifest.test.d.ts +4 -0
  278. package/dist/plugin-manifest.test.js +179 -0
  279. package/dist/plugin.d.ts +38 -5
  280. package/dist/plugin.js +267 -33
  281. package/dist/plugin.test.js +220 -3
  282. package/dist/registry.d.ts +4 -0
  283. package/dist/registry.js +2 -0
  284. package/dist/runtime-detect.d.ts +21 -0
  285. package/dist/runtime-detect.js +32 -0
  286. package/dist/runtime-detect.test.d.ts +1 -0
  287. package/dist/runtime-detect.test.js +27 -0
  288. package/dist/runtime.js +1 -1
  289. package/dist/serialization.d.ts +2 -0
  290. package/dist/serialization.js +6 -0
  291. package/dist/types.d.ts +1 -0
  292. package/dist/update-check.d.ts +22 -0
  293. package/dist/update-check.js +112 -0
  294. package/dist/weixin-download.test.d.ts +1 -0
  295. package/dist/weixin-download.test.js +30 -0
  296. package/dist/weread-private-api-regression.test.d.ts +1 -0
  297. package/dist/weread-private-api-regression.test.js +122 -0
  298. package/dist/weread-search-regression.test.d.ts +1 -0
  299. package/dist/weread-search-regression.test.js +39 -0
  300. package/dist/yaml-schema.d.ts +3 -0
  301. package/dist/yaml-schema.js +18 -1
  302. package/docs/.vitepress/config.mts +17 -0
  303. package/docs/adapters/browser/36kr.md +47 -0
  304. package/docs/adapters/browser/douban.md +14 -0
  305. package/docs/adapters/browser/douyin.md +75 -0
  306. package/docs/adapters/browser/imdb.md +47 -0
  307. package/docs/adapters/browser/jd.md +2 -2
  308. package/docs/adapters/browser/linux-do.md +181 -20
  309. package/docs/adapters/browser/paperreview.md +43 -0
  310. package/docs/adapters/browser/producthunt.md +49 -0
  311. package/docs/adapters/browser/twitter.md +6 -0
  312. package/docs/adapters/desktop/chatgpt.md +5 -0
  313. package/docs/adapters/index.md +12 -3
  314. package/docs/advanced/download.md +4 -0
  315. package/docs/advanced/rate-limiter-plugin.md +99 -0
  316. package/docs/guide/electron-app-cli.md +200 -0
  317. package/docs/guide/getting-started.md +1 -0
  318. package/docs/guide/plugins.md +87 -0
  319. package/docs/zh/guide/electron-app-cli.md +188 -0
  320. package/docs/zh/guide/getting-started.md +1 -0
  321. package/docs/zh/guide/plugins.md +65 -0
  322. package/extension/dist/background.js +508 -518
  323. package/extension/manifest.json +6 -2
  324. package/extension/package.json +2 -1
  325. package/extension/popup.html +84 -0
  326. package/extension/popup.js +25 -0
  327. package/extension/scripts/package-release.mjs +179 -0
  328. package/extension/src/background.ts +22 -1
  329. package/package.json +4 -1
  330. package/scripts/postinstall.js +10 -0
  331. package/src/browser/cdp.ts +2 -1
  332. package/src/browser/discover.ts +8 -3
  333. package/src/browser/errors.ts +13 -14
  334. package/src/browser/mcp.ts +2 -1
  335. package/src/build-manifest.test.ts +23 -0
  336. package/src/build-manifest.ts +40 -15
  337. package/src/capabilityRouting.ts +2 -1
  338. package/src/cli.ts +35 -3
  339. package/src/clis/36kr/article.ts +69 -0
  340. package/src/clis/36kr/hot.test.ts +19 -0
  341. package/src/clis/36kr/hot.ts +100 -0
  342. package/src/clis/36kr/news.test.ts +90 -0
  343. package/src/clis/36kr/news.ts +54 -0
  344. package/src/clis/36kr/search.ts +78 -0
  345. package/src/clis/apple-podcasts/search.ts +2 -1
  346. package/src/clis/arxiv/search.ts +2 -2
  347. package/src/clis/bbc/news.ts +0 -1
  348. package/src/clis/bilibili/comments.test.ts +102 -0
  349. package/src/clis/bilibili/comments.ts +44 -0
  350. package/src/clis/chatgpt/ask.ts +28 -14
  351. package/src/clis/chatgpt/ax.ts +180 -1
  352. package/src/clis/chatgpt/model.ts +27 -0
  353. package/src/clis/chatgpt/send.ts +16 -6
  354. package/src/clis/ctrip/search.ts +0 -1
  355. package/src/clis/douban/download.test.ts +196 -0
  356. package/src/clis/douban/download.ts +78 -0
  357. package/src/clis/douban/photos.ts +36 -0
  358. package/src/clis/douban/utils.test.ts +97 -0
  359. package/src/clis/douban/utils.ts +232 -1
  360. package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
  361. package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
  362. package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
  363. package/src/clis/douyin/_shared/creation-id.ts +8 -0
  364. package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
  365. package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
  366. package/src/clis/douyin/_shared/sts2.ts +20 -0
  367. package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
  368. package/src/clis/douyin/_shared/text-extra.ts +33 -0
  369. package/src/clis/douyin/_shared/timing.test.ts +38 -0
  370. package/src/clis/douyin/_shared/timing.ts +22 -0
  371. package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
  372. package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
  373. package/src/clis/douyin/_shared/tos-upload.ts +444 -0
  374. package/src/clis/douyin/_shared/transcode.test.ts +117 -0
  375. package/src/clis/douyin/_shared/transcode.ts +78 -0
  376. package/src/clis/douyin/_shared/types.ts +29 -0
  377. package/src/clis/douyin/activities.test.ts +25 -0
  378. package/src/clis/douyin/activities.ts +23 -0
  379. package/src/clis/douyin/collections.test.ts +26 -0
  380. package/src/clis/douyin/collections.ts +25 -0
  381. package/src/clis/douyin/delete.test.ts +12 -0
  382. package/src/clis/douyin/delete.ts +20 -0
  383. package/src/clis/douyin/draft.test.ts +12 -0
  384. package/src/clis/douyin/draft.ts +282 -0
  385. package/src/clis/douyin/drafts.test.ts +12 -0
  386. package/src/clis/douyin/drafts.ts +27 -0
  387. package/src/clis/douyin/hashtag.test.ts +28 -0
  388. package/src/clis/douyin/hashtag.ts +56 -0
  389. package/src/clis/douyin/location.test.ts +26 -0
  390. package/src/clis/douyin/location.ts +27 -0
  391. package/src/clis/douyin/profile.test.ts +12 -0
  392. package/src/clis/douyin/profile.ts +37 -0
  393. package/src/clis/douyin/publish.test.ts +45 -0
  394. package/src/clis/douyin/publish.ts +340 -0
  395. package/src/clis/douyin/stats.test.ts +25 -0
  396. package/src/clis/douyin/stats.ts +30 -0
  397. package/src/clis/douyin/update.test.ts +12 -0
  398. package/src/clis/douyin/update.ts +43 -0
  399. package/src/clis/douyin/videos.test.ts +12 -0
  400. package/src/clis/douyin/videos.ts +49 -0
  401. package/src/clis/hackernews/search.yaml +1 -1
  402. package/src/clis/imdb/person.ts +232 -0
  403. package/src/clis/imdb/reviews.ts +111 -0
  404. package/src/clis/imdb/search.ts +179 -0
  405. package/src/clis/imdb/title.ts +121 -0
  406. package/src/clis/imdb/top.ts +67 -0
  407. package/src/clis/imdb/trending.ts +66 -0
  408. package/src/clis/imdb/utils.test.ts +117 -0
  409. package/src/clis/imdb/utils.ts +305 -0
  410. package/src/clis/instagram/search.yaml +2 -1
  411. package/src/clis/jd/item.test.ts +18 -1
  412. package/src/clis/jd/item.ts +18 -15
  413. package/src/clis/linux-do/categories.yaml +38 -9
  414. package/src/clis/linux-do/category.ts +37 -0
  415. package/src/clis/linux-do/feed.test.ts +132 -0
  416. package/src/clis/linux-do/feed.ts +501 -0
  417. package/src/clis/linux-do/hot.ts +26 -0
  418. package/src/clis/linux-do/latest.ts +19 -0
  419. package/src/clis/linux-do/search.yaml +3 -1
  420. package/src/clis/linux-do/tags.yaml +41 -0
  421. package/src/clis/linux-do/topic.yaml +41 -3
  422. package/src/clis/linux-do/user-posts.yaml +67 -0
  423. package/src/clis/linux-do/user-topics.yaml +54 -0
  424. package/src/clis/medium/search.ts +1 -1
  425. package/src/clis/paperreview/commands.test.ts +283 -0
  426. package/src/clis/paperreview/feedback.ts +64 -0
  427. package/src/clis/paperreview/review.ts +47 -0
  428. package/src/clis/paperreview/submit.ts +119 -0
  429. package/src/clis/paperreview/utils.test.ts +68 -0
  430. package/src/clis/paperreview/utils.ts +276 -0
  431. package/src/clis/producthunt/browse.ts +109 -0
  432. package/src/clis/producthunt/hot.ts +127 -0
  433. package/src/clis/producthunt/posts.ts +29 -0
  434. package/src/clis/producthunt/today.ts +37 -0
  435. package/src/clis/producthunt/utils.test.ts +72 -0
  436. package/src/clis/producthunt/utils.ts +122 -0
  437. package/src/clis/reuters/search.ts +0 -1
  438. package/src/clis/twitter/article.ts +5 -28
  439. package/src/clis/twitter/likes.test.ts +91 -0
  440. package/src/clis/twitter/likes.ts +256 -0
  441. package/src/clis/twitter/profile.ts +5 -28
  442. package/src/clis/twitter/search.test.ts +71 -2
  443. package/src/clis/twitter/search.ts +8 -4
  444. package/src/clis/twitter/shared.ts +45 -0
  445. package/src/clis/twitter/timeline.ts +2 -13
  446. package/src/clis/weibo/comments.ts +54 -0
  447. package/src/clis/weibo/feed.ts +57 -0
  448. package/src/clis/weibo/hot.ts +0 -1
  449. package/src/clis/weibo/me.ts +77 -0
  450. package/src/clis/weibo/post.ts +77 -0
  451. package/src/clis/weibo/user.ts +64 -0
  452. package/src/clis/weibo/utils.ts +32 -0
  453. package/src/clis/weixin/download.ts +114 -20
  454. package/src/clis/weread/book.ts +2 -2
  455. package/src/clis/weread/commands.test.ts +57 -0
  456. package/src/clis/weread/highlights.ts +2 -2
  457. package/src/clis/weread/notebooks.ts +2 -2
  458. package/src/clis/weread/notes.ts +3 -3
  459. package/src/clis/weread/search.ts +3 -2
  460. package/src/clis/weread/shelf.ts +2 -2
  461. package/src/clis/weread/utils.test.ts +1 -32
  462. package/src/clis/weread/utils.ts +41 -16
  463. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  464. package/src/clis/xiaohongshu/comments.ts +81 -0
  465. package/src/clis/xiaohongshu/publish.test.ts +137 -0
  466. package/src/clis/xiaohongshu/publish.ts +129 -18
  467. package/src/clis/xueqiu/search.yaml +2 -1
  468. package/src/clis/yahoo-finance/quote.ts +0 -1
  469. package/src/clis/youtube/channel.ts +155 -0
  470. package/src/clis/youtube/comments.ts +97 -0
  471. package/src/clis/youtube/search.ts +0 -1
  472. package/src/clis/zhihu/search.yaml +2 -1
  473. package/src/commanderAdapter.test.ts +78 -0
  474. package/src/commanderAdapter.ts +188 -24
  475. package/src/daemon.ts +19 -1
  476. package/src/discovery.ts +8 -15
  477. package/src/doctor.ts +13 -2
  478. package/src/download/index.test.ts +14 -4
  479. package/src/download/index.ts +67 -55
  480. package/src/errors.ts +25 -66
  481. package/src/execution.ts +28 -3
  482. package/src/external-clis.yaml +0 -17
  483. package/src/hooks.ts +1 -0
  484. package/src/main.ts +6 -0
  485. package/src/output.ts +3 -1
  486. package/src/pipeline/executor.ts +4 -6
  487. package/src/plugin-manifest.test.ts +223 -0
  488. package/src/plugin-manifest.ts +206 -0
  489. package/src/plugin.test.ts +246 -2
  490. package/src/plugin.ts +338 -36
  491. package/src/registry.ts +6 -1
  492. package/src/runtime-detect.test.ts +30 -0
  493. package/src/runtime-detect.ts +36 -0
  494. package/src/runtime.ts +1 -1
  495. package/src/serialization.ts +4 -0
  496. package/src/types.ts +1 -0
  497. package/src/update-check.ts +114 -0
  498. package/src/weixin-download.test.ts +64 -0
  499. package/src/weread-private-api-regression.test.ts +150 -0
  500. package/src/weread-search-regression.test.ts +44 -0
  501. package/src/yaml-schema.ts +20 -0
  502. package/tests/e2e/browser-auth.test.ts +13 -9
  503. package/tests/e2e/browser-public-extended.test.ts +162 -0
  504. package/tests/e2e/browser-public.test.ts +55 -136
  505. package/tests/e2e/helpers.ts +2 -1
  506. package/tests/e2e/public-commands.test.ts +37 -3
  507. package/tests/smoke/api-health.test.ts +1 -1
  508. package/vitest.config.ts +34 -17
  509. package/dist/clis/linux-do/category.yaml +0 -51
  510. package/dist/clis/linux-do/hot.yaml +0 -50
  511. package/dist/clis/linux-do/latest.yaml +0 -40
  512. package/src/clis/linux-do/category.yaml +0 -51
  513. package/src/clis/linux-do/hot.yaml +0 -50
  514. package/src/clis/linux-do/latest.yaml +0 -40
@@ -16,7 +16,32 @@ import { type CliCommand, fullName, getRegistry } from './registry.js';
16
16
  import { formatRegistryHelpText } from './serialization.js';
17
17
  import { render as renderOutput } from './output.js';
18
18
  import { executeCommand } from './execution.js';
19
- import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
19
+ import {
20
+ CliError,
21
+ ERROR_ICONS,
22
+ getErrorMessage,
23
+ BrowserConnectError,
24
+ AuthRequiredError,
25
+ TimeoutError,
26
+ SelectorError,
27
+ EmptyResultError,
28
+ ArgumentError,
29
+ AdapterLoadError,
30
+ CommandExecutionError,
31
+ } from './errors.js';
32
+ import { checkDaemonStatus } from './browser/discover.js';
33
+
34
+ export function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown {
35
+ if (argType !== 'bool') return value;
36
+ if (typeof value === 'boolean') return value;
37
+ if (value == null || value === '') return false;
38
+
39
+ const normalized = String(value).trim().toLowerCase();
40
+ if (normalized === 'true') return true;
41
+ if (normalized === 'false') return false;
42
+
43
+ throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
44
+ }
20
45
 
21
46
  /**
22
47
  * Register a single CliCommand as a Commander subcommand.
@@ -24,7 +49,8 @@ import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
24
49
  export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): void {
25
50
  if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) return;
26
51
 
27
- const subCmd = siteCmd.command(cmd.name).description(cmd.description);
52
+ const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
53
+ const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
28
54
 
29
55
  // Register positional args first, then named options
30
56
  const positionalArgs: typeof cmd.args = [];
@@ -51,24 +77,29 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
51
77
  const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts as Record<string, unknown> : {};
52
78
  const startTime = Date.now();
53
79
 
54
- // ── Collect kwargs ──────────────────────────────────────────────────
55
- const kwargs: Record<string, unknown> = {};
56
- for (let i = 0; i < positionalArgs.length; i++) {
57
- const v = actionArgs[i];
58
- if (v !== undefined) kwargs[positionalArgs[i].name] = v;
59
- }
60
- for (const arg of cmd.args) {
61
- if (arg.positional) continue;
62
- const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
63
- const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
64
- if (v !== undefined) kwargs[arg.name] = v;
65
- }
66
-
67
80
  // ── Execute + render ────────────────────────────────────────────────
68
81
  try {
82
+ // ── Collect kwargs ────────────────────────────────────────────────
83
+ const kwargs: Record<string, unknown> = {};
84
+ for (let i = 0; i < positionalArgs.length; i++) {
85
+ const v = actionArgs[i];
86
+ if (v !== undefined) kwargs[positionalArgs[i].name] = v;
87
+ }
88
+ for (const arg of cmd.args) {
89
+ if (arg.positional) continue;
90
+ const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
91
+ const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
92
+ if (v !== undefined) kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
93
+ }
94
+
69
95
  const verbose = optionsRecord.verbose === true;
70
96
  const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
71
97
  if (verbose) process.env.OPENCLI_VERBOSE = '1';
98
+ if (cmd.deprecated) {
99
+ const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
100
+ const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
101
+ console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
102
+ }
72
103
 
73
104
  const result = await executeCommand(cmd, kwargs, verbose);
74
105
 
@@ -85,20 +116,153 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
85
116
  footerExtra: resolved.footerExtra?.(kwargs),
86
117
  });
87
118
  } catch (err) {
88
- if (err instanceof CliError) {
89
- const icon = ERROR_ICONS[err.code] ?? '⚠️';
90
- console.error(chalk.red(`${icon} ${err.message}`));
91
- if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
92
- } else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
93
- console.error(chalk.red(err.stack));
94
- } else {
95
- console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
96
- }
119
+ await renderError(err, fullName(cmd), optionsRecord.verbose === true);
97
120
  process.exitCode = 1;
98
121
  }
99
122
  });
100
123
  }
101
124
 
125
+ // ── Error rendering ──────────────────────────────────────────────────────────
126
+
127
+ const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
128
+
129
+ /** Pattern-based classifier for untyped errors thrown by adapters. */
130
+ function classifyGenericError(msg: string): 'auth' | 'http' | 'not-found' | 'other' {
131
+ const m = msg.toLowerCase();
132
+ if (/not logged in|login required|please log in|未登录|请先登录|authentication required|cookie expired/.test(m)) return 'auth';
133
+ // Match "HTTP 404", "status: 500", "status 403", bare "404 Not Found", etc.
134
+ if (/\b(status[: ]+)?[45]\d{2}\b|http[/ ][45]\d{2}/.test(m)) return 'http';
135
+ if (/not found|未找到|could not find|no .+ found/.test(m)) return 'not-found';
136
+ return 'other';
137
+ }
138
+
139
+ /** Render a status line for BrowserConnectError based on real-time or kind-derived state. */
140
+ function renderBridgeStatus(running: boolean, extensionConnected: boolean): void {
141
+ const ok = chalk.green('✓');
142
+ const fail = chalk.red('✗');
143
+ console.error(` Daemon ${running ? ok : fail} ${running ? 'running' : 'not running'}`);
144
+ console.error(` Extension ${extensionConnected ? ok : fail} ${extensionConnected ? 'connected' : 'not connected'}`);
145
+ console.error();
146
+ if (!running) {
147
+ console.error(chalk.yellow(' Run the command again — daemon should auto-start.'));
148
+ console.error(chalk.dim(' Still failing? Run: opencli doctor'));
149
+ } else if (!extensionConnected) {
150
+ console.error(chalk.yellow(' Install the Browser Bridge extension to continue:'));
151
+ console.error(chalk.dim(' 1. Download from github.com/jackwener/opencli/releases'));
152
+ console.error(chalk.dim(' 2. chrome://extensions → Enable Developer Mode → Load unpacked'));
153
+ } else {
154
+ console.error(chalk.yellow(' Connection failed despite extension being active.'));
155
+ console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
156
+ }
157
+ }
158
+
159
+ async function renderError(err: unknown, cmdName: string, verbose: boolean): Promise<void> {
160
+ // ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
161
+ if (err instanceof BrowserConnectError) {
162
+ console.error(chalk.red('🔌 Browser Bridge not connected'));
163
+ console.error();
164
+ try {
165
+ // 300ms matches execution.ts — localhost responds in <50ms when running.
166
+ const status = await checkDaemonStatus({ timeout: 300 });
167
+ renderBridgeStatus(status.running, status.extensionConnected);
168
+ } catch (_statusErr) {
169
+ // checkDaemonStatus itself failed — derive best-guess state from kind.
170
+ const running = err.kind !== 'daemon-not-running';
171
+ const extensionConnected = err.kind === 'command-failed';
172
+ renderBridgeStatus(running, extensionConnected);
173
+ }
174
+ return;
175
+ }
176
+
177
+ // ── AuthRequiredError ─────────────────────────────────────────────────
178
+ if (err instanceof AuthRequiredError) {
179
+ console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
180
+ // Respect custom hints set by the adapter; fall back to generic guidance.
181
+ console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
182
+ return;
183
+ }
184
+
185
+ // ── TimeoutError ──────────────────────────────────────────────────────
186
+ if (err instanceof TimeoutError) {
187
+ console.error(chalk.red(`⏱ ${err.message}`));
188
+ console.error(chalk.yellow('→ Try again, or raise the limit:'));
189
+ console.error(chalk.dim(` OPENCLI_BROWSER_COMMAND_TIMEOUT=60 ${cmdName}`));
190
+ return;
191
+ }
192
+
193
+ // ── SelectorError / EmptyResultError: likely outdated adapter ─────────
194
+ if (err instanceof SelectorError || err instanceof EmptyResultError) {
195
+ const icon = ERROR_ICONS[err.code] ?? '⚠️';
196
+ console.error(chalk.red(`${icon} ${err.message}`));
197
+ console.error(chalk.yellow('→ The page structure may have changed — this adapter may be outdated.'));
198
+ console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
199
+ console.error(chalk.dim(` Report: ${ISSUES_URL}`));
200
+ return;
201
+ }
202
+
203
+ // ── ArgumentError ─────────────────────────────────────────────────────
204
+ if (err instanceof ArgumentError) {
205
+ console.error(chalk.red(`❌ ${err.message}`));
206
+ if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
207
+ return;
208
+ }
209
+
210
+ // ── AdapterLoadError ──────────────────────────────────────────────────
211
+ if (err instanceof AdapterLoadError) {
212
+ console.error(chalk.red(`📦 ${err.message}`));
213
+ if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
214
+ return;
215
+ }
216
+
217
+ // ── CommandExecutionError ─────────────────────────────────────────────
218
+ if (err instanceof CommandExecutionError) {
219
+ console.error(chalk.red(`💥 ${err.message}`));
220
+ if (err.hint) {
221
+ console.error(chalk.yellow(`→ ${err.hint}`));
222
+ } else {
223
+ console.error(chalk.dim(` Add --verbose for details, or report: ${ISSUES_URL}`));
224
+ }
225
+ return;
226
+ }
227
+
228
+ // ── Other typed CliError (fallback for future codes) ──────────────────
229
+ if (err instanceof CliError) {
230
+ const icon = ERROR_ICONS[err.code] ?? '⚠️';
231
+ console.error(chalk.red(`${icon} ${err.message}`));
232
+ if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
233
+ return;
234
+ }
235
+
236
+ // ── Generic Error from adapters: classify by message pattern ──────────
237
+ const msg = getErrorMessage(err);
238
+ const kind = classifyGenericError(msg);
239
+
240
+ if (kind === 'auth') {
241
+ console.error(chalk.red(`🔒 ${msg}`));
242
+ console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
243
+ return;
244
+ }
245
+ if (kind === 'http') {
246
+ console.error(chalk.red(`🌐 ${msg}`));
247
+ console.error(chalk.yellow('→ Check your login status, or the site may be temporarily unavailable.'));
248
+ return;
249
+ }
250
+ if (kind === 'not-found') {
251
+ console.error(chalk.red(`📭 ${msg}`));
252
+ console.error(chalk.yellow('→ The resource was not found. The adapter or page structure may have changed.'));
253
+ console.error(chalk.dim(` Report: ${ISSUES_URL}`));
254
+ return;
255
+ }
256
+
257
+ // ── Unknown error: show stack in verbose mode ─────────────────────────
258
+ if (verbose && err instanceof Error && err.stack) {
259
+ console.error(chalk.red(err.stack));
260
+ } else {
261
+ console.error(chalk.red(`💥 Unexpected error: ${msg}`));
262
+ console.error(chalk.dim(` Run with --verbose for details, or report: ${ISSUES_URL}`));
263
+ }
264
+ }
265
+
102
266
  /**
103
267
  * Register all commands from the registry onto a Commander program.
104
268
  */
package/src/daemon.ts CHANGED
@@ -29,6 +29,7 @@ const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
29
29
  // ─── State ───────────────────────────────────────────────────────────
30
30
 
31
31
  let extensionWs: WebSocket | null = null;
32
+ let extensionVersion: string | null = null;
32
33
  const pending = new Map<string, {
33
34
  resolve: (data: unknown) => void;
34
35
  reject: (error: Error) => void;
@@ -117,6 +118,7 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
117
118
  jsonResponse(res, 200, {
118
119
  ok: true,
119
120
  extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
121
+ extensionVersion,
120
122
  pending: pending.size,
121
123
  });
122
124
  return;
@@ -222,6 +224,12 @@ wss.on('connection', (ws: WebSocket) => {
222
224
  try {
223
225
  const msg = JSON.parse(data.toString());
224
226
 
227
+ // Handle hello message from extension (version handshake)
228
+ if (msg.type === 'hello') {
229
+ extensionVersion = typeof msg.version === 'string' ? msg.version : null;
230
+ return;
231
+ }
232
+
225
233
  // Handle log messages from extension
226
234
  if (msg.type === 'log') {
227
235
  const prefix = msg.level === 'error' ? '❌' : msg.level === 'warn' ? '⚠️' : '📋';
@@ -247,6 +255,7 @@ wss.on('connection', (ws: WebSocket) => {
247
255
  clearInterval(heartbeatInterval);
248
256
  if (extensionWs === ws) {
249
257
  extensionWs = null;
258
+ extensionVersion = null;
250
259
  // Reject all pending requests since the extension is gone
251
260
  for (const [id, p] of pending) {
252
261
  clearTimeout(p.timer);
@@ -258,7 +267,16 @@ wss.on('connection', (ws: WebSocket) => {
258
267
 
259
268
  ws.on('error', () => {
260
269
  clearInterval(heartbeatInterval);
261
- if (extensionWs === ws) extensionWs = null;
270
+ if (extensionWs === ws) {
271
+ extensionWs = null;
272
+ extensionVersion = null;
273
+ // Reject pending requests in case 'close' does not follow this 'error'
274
+ for (const [, p] of pending) {
275
+ clearTimeout(p.timer);
276
+ p.reject(new Error('Extension disconnected'));
277
+ }
278
+ pending.clear();
279
+ }
262
280
  });
263
281
  });
264
282
 
package/src/discovery.ts CHANGED
@@ -23,7 +23,7 @@ export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
23
23
  /** Matches files that register commands via cli() or lifecycle hooks */
24
24
  const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
25
25
 
26
- import type { YamlCliDefinition } from './yaml-schema.js';
26
+ import { type YamlCliDefinition, parseYamlArgs } from './yaml-schema.js';
27
27
 
28
28
  function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
29
29
  if (!rawStrategy) return fallback;
@@ -77,6 +77,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
77
77
  pipeline: entry.pipeline,
78
78
  timeoutSeconds: entry.timeout,
79
79
  source: `manifest:${entry.site}/${entry.name}`,
80
+ deprecated: entry.deprecated,
81
+ replacedBy: entry.replacedBy,
80
82
  navigateBefore: entry.navigateBefore,
81
83
  };
82
84
  registerCommand(cmd);
@@ -96,6 +98,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
96
98
  columns: entry.columns,
97
99
  timeoutSeconds: entry.timeout,
98
100
  source: modulePath,
101
+ deprecated: entry.deprecated,
102
+ replacedBy: entry.replacedBy,
99
103
  navigateBefore: entry.navigateBefore,
100
104
  _lazy: true,
101
105
  _modulePath: modulePath,
@@ -158,20 +162,7 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
158
162
  const strategy = parseStrategy(strategyStr);
159
163
  const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
160
164
 
161
- const args: Arg[] = [];
162
- if (cliDef.args && typeof cliDef.args === 'object') {
163
- for (const [argName, argDef] of Object.entries(cliDef.args)) {
164
- args.push({
165
- name: argName,
166
- type: argDef?.type ?? 'str',
167
- default: argDef?.default,
168
- required: argDef?.required ?? false,
169
- positional: argDef?.positional ?? false,
170
- help: argDef?.description ?? argDef?.help ?? '',
171
- choices: argDef?.choices,
172
- });
173
- }
174
- }
165
+ const args = parseYamlArgs(cliDef.args);
175
166
 
176
167
  const cmd: CliCommand = {
177
168
  site,
@@ -185,6 +176,8 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
185
176
  pipeline: cliDef.pipeline,
186
177
  timeoutSeconds: cliDef.timeout,
187
178
  source: filePath,
179
+ deprecated: (cliDef as Record<string, unknown>).deprecated as boolean | string | undefined,
180
+ replacedBy: (cliDef as Record<string, unknown>).replacedBy as string | undefined,
188
181
  navigateBefore: cliDef.navigateBefore,
189
182
  };
190
183
 
package/src/doctor.ts CHANGED
@@ -11,6 +11,7 @@ import { checkDaemonStatus } from './browser/discover.js';
11
11
  import { BrowserBridge } from './browser/index.js';
12
12
  import { listSessions } from './browser/daemon-client.js';
13
13
  import { getErrorMessage } from './errors.js';
14
+ import { getRuntimeLabel } from './runtime-detect.js';
14
15
 
15
16
  export type DoctorOptions = {
16
17
  fix?: boolean;
@@ -30,6 +31,7 @@ export type DoctorReport = {
30
31
  cliVersion?: string;
31
32
  daemonRunning: boolean;
32
33
  extensionConnected: boolean;
34
+ extensionVersion?: string;
33
35
  connectivity?: ConnectivityResult;
34
36
  sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
35
37
  issues: string[];
@@ -94,10 +96,18 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
94
96
  issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
95
97
  }
96
98
 
99
+ if (status.extensionVersion && opts.cliVersion && status.extensionVersion !== opts.cliVersion) {
100
+ issues.push(
101
+ `Extension version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
102
+ ' Download the latest extension from: https://github.com/jackwener/opencli/releases',
103
+ );
104
+ }
105
+
97
106
  return {
98
107
  cliVersion: opts.cliVersion,
99
108
  daemonRunning: status.running,
100
109
  extensionConnected: status.extensionConnected,
110
+ extensionVersion: status.extensionVersion,
101
111
  connectivity,
102
112
  sessions,
103
113
  issues,
@@ -105,7 +115,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
105
115
  }
106
116
 
107
117
  export function renderBrowserDoctorReport(report: DoctorReport): string {
108
- const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
118
+ const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
109
119
 
110
120
  // Daemon status
111
121
  const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
@@ -113,7 +123,8 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
113
123
 
114
124
  // Extension status
115
125
  const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
116
- lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}`);
126
+ const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
127
+ lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
117
128
 
118
129
  // Connectivity
119
130
  if (report.connectivity) {
@@ -6,12 +6,17 @@ import { afterEach, describe, expect, it } from 'vitest';
6
6
  import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
7
7
 
8
8
  const servers: http.Server[] = [];
9
+ const tempDirs: string[] = [];
9
10
 
10
11
  afterEach(async () => {
11
12
  await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
12
13
  server.close((err) => (err ? reject(err) : resolve()));
13
14
  })));
14
15
  servers.length = 0;
16
+ for (const dir of tempDirs) {
17
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
18
+ }
19
+ tempDirs.length = 0;
15
20
  });
16
21
 
17
22
  async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> {
@@ -25,7 +30,9 @@ async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'
25
30
  return `http://${hostname}:${address.port}`;
26
31
  }
27
32
 
28
- describe('download helpers', () => {
33
+ // Windows Defender can briefly lock newly-written .tmp files, causing EPERM.
34
+ // Retry once to handle this flakiness.
35
+ describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
29
36
  it('resolves relative redirects against the original URL', () => {
30
37
  expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
31
38
  expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
@@ -45,7 +52,8 @@ describe('download helpers', () => {
45
52
  res.end();
46
53
  });
47
54
 
48
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
55
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
56
+ tempDirs.push(tempDir);
49
57
  const destPath = path.join(tempDir, 'file.txt');
50
58
  const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
51
59
 
@@ -71,7 +79,8 @@ describe('download helpers', () => {
71
79
  res.end();
72
80
  });
73
81
 
74
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
82
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
83
+ tempDirs.push(tempDir);
75
84
  const destPath = path.join(tempDir, 'redirect.txt');
76
85
  const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
77
86
 
@@ -94,7 +103,8 @@ describe('download helpers', () => {
94
103
  res.end();
95
104
  });
96
105
 
97
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
106
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
107
+ tempDirs.push(tempDir);
98
108
  const destPath = path.join(tempDir, 'redirect-header.txt');
99
109
  const result = await httpDownload(`${redirectUrl}/start`, destPath, {
100
110
  headers: { Cookie: 'sid=header-cookie' },
@@ -8,6 +8,8 @@ import * as path from 'node:path';
8
8
  import * as https from 'node:https';
9
9
  import * as http from 'node:http';
10
10
  import * as os from 'node:os';
11
+ import { Transform } from 'node:stream';
12
+ import { pipeline } from 'node:stream/promises';
11
13
  import { URL } from 'node:url';
12
14
  import type { ProgressBar } from './progress.js';
13
15
  import { isBinaryInstalled } from '../external.js';
@@ -99,74 +101,84 @@ export async function httpDownload(
99
101
  requestHeaders['Cookie'] = cookies;
100
102
  }
101
103
 
102
- // Ensure directory exists
103
- const dir = path.dirname(destPath);
104
- fs.mkdirSync(dir, { recursive: true });
105
-
106
104
  const tempPath = `${destPath}.tmp`;
107
- const file = fs.createWriteStream(tempPath);
105
+ let settled = false;
108
106
 
109
- const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
110
- // Handle redirects
111
- if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
112
- file.close();
113
- if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
114
- if (redirectCount >= maxRedirects) {
115
- resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
116
- return;
117
- }
118
- const redirectUrl = resolveRedirectUrl(url, response.headers.location);
119
- const originalHost = new URL(url).hostname;
120
- const redirectHost = new URL(redirectUrl).hostname;
121
- // Do not forward cookies when a redirect crosses host boundaries.
122
- const redirectOptions = originalHost === redirectHost
123
- ? options
124
- : { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
125
- httpDownload(
126
- redirectUrl,
127
- destPath,
128
- redirectOptions,
129
- redirectCount + 1,
130
- ).then(resolve);
131
- return;
132
- }
107
+ const finish = (result: { success: boolean; size: number; error?: string }) => {
108
+ if (settled) return;
109
+ settled = true;
110
+ resolve(result);
111
+ };
133
112
 
134
- if (response.statusCode !== 200) {
135
- file.close();
136
- if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
137
- resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
138
- return;
113
+ const cleanupTempFile = async () => {
114
+ try {
115
+ await fs.promises.rm(tempPath, { force: true });
116
+ } catch {
117
+ // Ignore cleanup errors so the original failure is preserved.
139
118
  }
119
+ };
140
120
 
141
- const totalSize = parseInt(response.headers['content-length'] || '0', 10);
142
- let received = 0;
143
-
144
- response.on('data', (chunk: Buffer) => {
145
- received += chunk.length;
146
- if (onProgress) onProgress(received, totalSize);
147
- });
121
+ const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
122
+ void (async () => {
123
+ // Handle redirects before creating any file handles.
124
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
125
+ response.resume();
126
+ if (redirectCount >= maxRedirects) {
127
+ finish({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
128
+ return;
129
+ }
130
+ const redirectUrl = resolveRedirectUrl(url, response.headers.location);
131
+ const originalHost = new URL(url).hostname;
132
+ const redirectHost = new URL(redirectUrl).hostname;
133
+ const redirectOptions = originalHost === redirectHost
134
+ ? options
135
+ : { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
136
+ finish(await httpDownload(
137
+ redirectUrl,
138
+ destPath,
139
+ redirectOptions,
140
+ redirectCount + 1,
141
+ ));
142
+ return;
143
+ }
148
144
 
149
- response.pipe(file);
145
+ if (response.statusCode !== 200) {
146
+ response.resume();
147
+ finish({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
148
+ return;
149
+ }
150
150
 
151
- file.on('finish', () => {
152
- file.close();
153
- // Rename temp file to final destination
154
- fs.renameSync(tempPath, destPath);
155
- resolve({ success: true, size: received });
156
- });
151
+ const totalSize = parseInt(response.headers['content-length'] || '0', 10);
152
+ let received = 0;
153
+ const progressStream = new Transform({
154
+ transform(chunk, _encoding, callback) {
155
+ received += chunk.length;
156
+ if (onProgress) onProgress(received, totalSize);
157
+ callback(null, chunk);
158
+ },
159
+ });
160
+
161
+ try {
162
+ await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
163
+ await pipeline(response, progressStream, fs.createWriteStream(tempPath));
164
+ await fs.promises.rename(tempPath, destPath);
165
+ finish({ success: true, size: received });
166
+ } catch (err) {
167
+ await cleanupTempFile();
168
+ finish({ success: false, size: 0, error: getErrorMessage(err) });
169
+ }
170
+ })();
157
171
  });
158
172
 
159
173
  request.on('error', (err) => {
160
- file.close();
161
- if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
162
- resolve({ success: false, size: 0, error: err.message });
174
+ void (async () => {
175
+ await cleanupTempFile();
176
+ finish({ success: false, size: 0, error: err.message });
177
+ })();
163
178
  });
164
179
 
165
180
  request.on('timeout', () => {
166
- request.destroy();
167
- file.close();
168
- if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
169
- resolve({ success: false, size: 0, error: 'Timeout' });
181
+ request.destroy(new Error('Timeout'));
170
182
  });
171
183
  });
172
184
  }