@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
package/src/plugin.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * Plugin management: install, uninstall, and list plugins.
3
3
  *
4
4
  * Plugins live in ~/.opencli/plugins/<name>/.
5
- * Install source format: "github:user/repo"
5
+ * Monorepo clones live in ~/.opencli/monorepos/<repo-name>/.
6
+ * Install source format: "github:user/repo" or "github:user/repo/subplugin"
6
7
  */
7
8
 
8
9
  import * as fs from 'node:fs';
@@ -13,6 +14,13 @@ import { fileURLToPath } from 'node:url';
13
14
  import { PLUGINS_DIR } from './discovery.js';
14
15
  import { getErrorMessage } from './errors.js';
15
16
  import { log } from './logger.js';
17
+ import {
18
+ readPluginManifest,
19
+ isMonorepo,
20
+ getEnabledPlugins,
21
+ checkCompatibility,
22
+ type PluginManifest,
23
+ } from './plugin-manifest.js';
16
24
 
17
25
  const isWindows = process.platform === 'win32';
18
26
 
@@ -26,14 +34,27 @@ export function getLockFilePath(): string {
26
34
  return path.join(getHomeDir(), '.opencli', 'plugins.lock.json');
27
35
  }
28
36
 
37
+ /** Monorepo clones directory: ~/.opencli/monorepos/ */
38
+ export function getMonoreposDir(): string {
39
+ return path.join(getHomeDir(), '.opencli', 'monorepos');
40
+ }
41
+
29
42
  // Legacy const for backward compatibility (computed at load time)
30
43
  export const LOCK_FILE = path.join(os.homedir(), '.opencli', 'plugins.lock.json');
44
+ export const MONOREPOS_DIR = path.join(os.homedir(), '.opencli', 'monorepos');
31
45
 
32
46
  export interface LockEntry {
33
47
  source: string;
34
48
  commitHash: string;
35
49
  installedAt: string;
36
50
  updatedAt?: string;
51
+ /** Present when this plugin comes from a monorepo. */
52
+ monorepo?: {
53
+ /** Monorepo directory name under ~/.opencli/monorepos/ */
54
+ name: string;
55
+ /** Relative path of this sub-plugin within the monorepo. */
56
+ subPath: string;
57
+ };
37
58
  }
38
59
 
39
60
  export interface PluginInfo {
@@ -43,6 +64,10 @@ export interface PluginInfo {
43
64
  source?: string;
44
65
  version?: string;
45
66
  installedAt?: string;
67
+ /** If from a monorepo, the monorepo name. */
68
+ monorepoName?: string;
69
+ /** Description from opencli-plugin.json. */
70
+ description?: string;
46
71
  }
47
72
 
48
73
  // ── Validation helpers ──────────────────────────────────────────────────────
@@ -122,25 +147,23 @@ export function validatePluginStructure(pluginDir: string): ValidationResult {
122
147
  return { valid: errors.length === 0, errors };
123
148
  }
124
149
 
125
- /**
126
- * Shared post-install lifecycle: npm install → host symlink → TS transpile.
127
- * Called by both installPlugin() and updatePlugin().
128
- */
129
- function postInstallLifecycle(pluginDir: string): void {
130
- const pkgJsonPath = path.join(pluginDir, 'package.json');
150
+ function installDependencies(dir: string): void {
151
+ const pkgJsonPath = path.join(dir, 'package.json');
131
152
  if (!fs.existsSync(pkgJsonPath)) return;
132
153
 
133
154
  try {
134
155
  execFileSync('npm', ['install', '--omit=dev'], {
135
- cwd: pluginDir,
156
+ cwd: dir,
136
157
  encoding: 'utf-8',
137
158
  stdio: ['pipe', 'pipe', 'pipe'],
138
159
  ...(isWindows && { shell: true }),
139
160
  });
140
161
  } catch (err) {
141
- console.error(`[plugin] npm install failed in ${pluginDir}: ${err instanceof Error ? err.message : err}`);
162
+ throw new Error(`npm install failed in ${dir}: ${getErrorMessage(err)}`);
142
163
  }
164
+ }
143
165
 
166
+ function finalizePluginRuntime(pluginDir: string): void {
144
167
  // Symlink host opencli so TS plugins resolve '@jackwener/opencli/registry'
145
168
  // against the running host, not a stale npm-published version.
146
169
  linkHostOpencli(pluginDir);
@@ -149,33 +172,51 @@ function postInstallLifecycle(pluginDir: string): void {
149
172
  transpilePluginTs(pluginDir);
150
173
  }
151
174
 
175
+ /**
176
+ * Shared post-install lifecycle for standalone plugins.
177
+ */
178
+ function postInstallLifecycle(pluginDir: string): void {
179
+ installDependencies(pluginDir);
180
+ finalizePluginRuntime(pluginDir);
181
+ }
182
+
183
+ /**
184
+ * Monorepo lifecycle: install shared deps once at repo root, then finalize each sub-plugin.
185
+ */
186
+ function postInstallMonorepoLifecycle(repoDir: string, pluginDirs: string[]): void {
187
+ installDependencies(repoDir);
188
+ for (const pluginDir of pluginDirs) {
189
+ finalizePluginRuntime(pluginDir);
190
+ }
191
+ }
192
+
152
193
  /**
153
194
  * Install a plugin from a source.
154
- * Currently supports "github:user/repo" format (git clone wrapper).
195
+ * Supports:
196
+ * "github:user/repo" — single plugin or full monorepo
197
+ * "github:user/repo/subplugin" — specific sub-plugin from a monorepo
198
+ * "https://github.com/user/repo"
199
+ *
200
+ * Returns the installed plugin name(s).
155
201
  */
156
- export function installPlugin(source: string): string {
202
+ export function installPlugin(source: string): string | string[] {
157
203
  const parsed = parseSource(source);
158
204
  if (!parsed) {
159
205
  throw new Error(
160
206
  `Invalid plugin source: "${source}"\n` +
161
207
  `Supported formats:\n` +
162
208
  ` github:user/repo\n` +
209
+ ` github:user/repo/subplugin\n` +
163
210
  ` https://github.com/user/repo`
164
211
  );
165
212
  }
166
213
 
167
- const { cloneUrl, name } = parsed;
168
- const targetDir = path.join(PLUGINS_DIR, name);
169
-
170
- if (fs.existsSync(targetDir)) {
171
- throw new Error(`Plugin "${name}" is already installed at ${targetDir}`);
172
- }
173
-
174
- // Ensure plugins directory exists
175
- fs.mkdirSync(PLUGINS_DIR, { recursive: true });
214
+ const { cloneUrl, name: repoName, subPlugin } = parsed;
176
215
 
216
+ // Clone to a temporary location first so we can inspect the manifest
217
+ const tmpCloneDir = path.join(os.tmpdir(), `opencli-clone-${Date.now()}`);
177
218
  try {
178
- execFileSync('git', ['clone', '--depth', '1', cloneUrl, targetDir], {
219
+ execFileSync('git', ['clone', '--depth', '1', cloneUrl, tmpCloneDir], {
179
220
  encoding: 'utf-8',
180
221
  stdio: ['pipe', 'pipe', 'pipe'],
181
222
  });
@@ -183,19 +224,56 @@ export function installPlugin(source: string): string {
183
224
  throw new Error(`Failed to clone plugin: ${getErrorMessage(err)}`);
184
225
  }
185
226
 
186
- const validation = validatePluginStructure(targetDir);
227
+ try {
228
+ const manifest = readPluginManifest(tmpCloneDir);
229
+
230
+ // Check top-level compatibility
231
+ if (manifest?.opencli && !checkCompatibility(manifest.opencli)) {
232
+ throw new Error(
233
+ `Plugin requires opencli ${manifest.opencli}, but current version is incompatible.`
234
+ );
235
+ }
236
+
237
+ if (manifest && isMonorepo(manifest)) {
238
+ return installMonorepo(tmpCloneDir, cloneUrl, repoName, manifest, subPlugin);
239
+ }
240
+
241
+ // Single plugin mode
242
+ return installSinglePlugin(tmpCloneDir, cloneUrl, repoName, manifest);
243
+ } finally {
244
+ // Clean up temp clone (may already have been moved)
245
+ try { fs.rmSync(tmpCloneDir, { recursive: true, force: true }); } catch {}
246
+ }
247
+ }
248
+
249
+ /** Install a single (non-monorepo) plugin. */
250
+ function installSinglePlugin(
251
+ cloneDir: string,
252
+ cloneUrl: string,
253
+ name: string,
254
+ manifest: PluginManifest | null,
255
+ ): string {
256
+ const pluginName = manifest?.name ?? name;
257
+ const targetDir = path.join(PLUGINS_DIR, pluginName);
258
+
259
+ if (fs.existsSync(targetDir)) {
260
+ throw new Error(`Plugin "${pluginName}" is already installed at ${targetDir}`);
261
+ }
262
+
263
+ const validation = validatePluginStructure(cloneDir);
187
264
  if (!validation.valid) {
188
- // If validation fails, clean up the cloned directory and abort
189
- fs.rmSync(targetDir, { recursive: true, force: true });
190
265
  throw new Error(`Invalid plugin structure:\n- ${validation.errors.join('\n- ')}`);
191
266
  }
192
267
 
268
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
269
+ fs.renameSync(cloneDir, targetDir);
270
+
193
271
  postInstallLifecycle(targetDir);
194
272
 
195
273
  const commitHash = getCommitHash(targetDir);
196
274
  if (commitHash) {
197
275
  const lock = readLockFile();
198
- lock[name] = {
276
+ lock[pluginName] = {
199
277
  source: cloneUrl,
200
278
  commitHash,
201
279
  installedAt: new Date().toISOString(),
@@ -203,28 +281,160 @@ export function installPlugin(source: string): string {
203
281
  writeLockFile(lock);
204
282
  }
205
283
 
206
- return name;
284
+ return pluginName;
285
+ }
286
+
287
+ /** Install sub-plugins from a monorepo. */
288
+ function installMonorepo(
289
+ cloneDir: string,
290
+ cloneUrl: string,
291
+ repoName: string,
292
+ manifest: PluginManifest,
293
+ subPlugin?: string,
294
+ ): string[] {
295
+ const monoreposDir = getMonoreposDir();
296
+ const repoDir = path.join(monoreposDir, repoName);
297
+
298
+ // Move clone to permanent monorepos location (if not already there)
299
+ if (!fs.existsSync(repoDir)) {
300
+ fs.mkdirSync(monoreposDir, { recursive: true });
301
+ fs.renameSync(cloneDir, repoDir);
302
+ }
303
+
304
+ let pluginsToInstall = getEnabledPlugins(manifest);
305
+
306
+ // If a specific sub-plugin was requested, filter to just that one
307
+ if (subPlugin) {
308
+ pluginsToInstall = pluginsToInstall.filter((p) => p.name === subPlugin);
309
+ if (pluginsToInstall.length === 0) {
310
+ // Check if it exists but is disabled
311
+ const disabled = manifest.plugins?.[subPlugin];
312
+ if (disabled) {
313
+ throw new Error(`Sub-plugin "${subPlugin}" is disabled in the manifest.`);
314
+ }
315
+ throw new Error(
316
+ `Sub-plugin "${subPlugin}" not found in monorepo. Available: ${Object.keys(manifest.plugins ?? {}).join(', ')}`
317
+ );
318
+ }
319
+ }
320
+
321
+ const installedNames: string[] = [];
322
+ const lock = readLockFile();
323
+ const commitHash = getCommitHash(repoDir);
324
+ const eligiblePlugins: Array<{ name: string; entry: typeof pluginsToInstall[number]['entry']; subDir: string }> = [];
325
+
326
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
327
+
328
+ for (const { name, entry } of pluginsToInstall) {
329
+ // Check sub-plugin level compatibility (overrides top-level)
330
+ if (entry.opencli && !checkCompatibility(entry.opencli)) {
331
+ log.warn(`Skipping "${name}": requires opencli ${entry.opencli}`);
332
+ continue;
333
+ }
334
+
335
+ const subDir = path.join(repoDir, entry.path);
336
+ if (!fs.existsSync(subDir)) {
337
+ log.warn(`Skipping "${name}": path "${entry.path}" not found in repo.`);
338
+ continue;
339
+ }
340
+
341
+ const validation = validatePluginStructure(subDir);
342
+ if (!validation.valid) {
343
+ log.warn(`Skipping "${name}": invalid structure — ${validation.errors.join(', ')}`);
344
+ continue;
345
+ }
346
+
347
+ const linkPath = path.join(PLUGINS_DIR, name);
348
+ if (fs.existsSync(linkPath)) {
349
+ log.warn(`Skipping "${name}": already installed at ${linkPath}`);
350
+ continue;
351
+ }
352
+
353
+ eligiblePlugins.push({ name, entry, subDir });
354
+ }
355
+
356
+ if (eligiblePlugins.length > 0) {
357
+ postInstallMonorepoLifecycle(repoDir, eligiblePlugins.map((p) => p.subDir));
358
+ }
359
+
360
+ for (const { name, entry, subDir } of eligiblePlugins) {
361
+ const linkPath = path.join(PLUGINS_DIR, name);
362
+
363
+ // Create symlink (junction on Windows)
364
+ const linkType = isWindows ? 'junction' : 'dir';
365
+ fs.symlinkSync(subDir, linkPath, linkType);
366
+
367
+ if (commitHash) {
368
+ lock[name] = {
369
+ source: cloneUrl,
370
+ commitHash,
371
+ installedAt: new Date().toISOString(),
372
+ monorepo: { name: repoName, subPath: entry.path },
373
+ };
374
+ }
375
+
376
+ installedNames.push(name);
377
+ }
378
+
379
+ writeLockFile(lock);
380
+ return installedNames;
207
381
  }
208
382
 
209
383
  /**
210
384
  * Uninstall a plugin by name.
385
+ * For monorepo sub-plugins: removes symlink and cleans up the monorepo
386
+ * directory when no more sub-plugins reference it.
211
387
  */
212
388
  export function uninstallPlugin(name: string): void {
213
389
  const targetDir = path.join(PLUGINS_DIR, name);
214
390
  if (!fs.existsSync(targetDir)) {
215
391
  throw new Error(`Plugin "${name}" is not installed.`);
216
392
  }
217
- fs.rmSync(targetDir, { recursive: true, force: true });
218
393
 
219
394
  const lock = readLockFile();
220
- if (lock[name]) {
395
+ const lockEntry = lock[name];
396
+
397
+ // Check if this is a symlink (monorepo sub-plugin)
398
+ const isSymlink = isSymlinkSync(targetDir);
399
+
400
+ if (isSymlink) {
401
+ // Remove symlink only (not the actual directory)
402
+ fs.unlinkSync(targetDir);
403
+ } else {
404
+ fs.rmSync(targetDir, { recursive: true, force: true });
405
+ }
406
+
407
+ // Clean up monorepo directory if no more sub-plugins reference it
408
+ if (lockEntry?.monorepo) {
221
409
  delete lock[name];
222
- writeLockFile(lock);
410
+ const monoName = lockEntry.monorepo.name;
411
+ const stillReferenced = Object.values(lock).some(
412
+ (entry) => entry.monorepo?.name === monoName,
413
+ );
414
+ if (!stillReferenced) {
415
+ const monoDir = path.join(getMonoreposDir(), monoName);
416
+ try { fs.rmSync(monoDir, { recursive: true, force: true }); } catch {}
417
+ }
418
+ } else if (lock[name]) {
419
+ delete lock[name];
420
+ }
421
+
422
+ writeLockFile(lock);
423
+ }
424
+
425
+ /** Synchronous check if a path is a symlink. */
426
+ function isSymlinkSync(p: string): boolean {
427
+ try {
428
+ return fs.lstatSync(p).isSymbolicLink();
429
+ } catch {
430
+ return false;
223
431
  }
224
432
  }
225
433
 
226
434
  /**
227
435
  * Update a plugin by name (git pull + re-install lifecycle).
436
+ * For monorepo sub-plugins: pulls the monorepo root and re-runs lifecycle
437
+ * for all sub-plugins from the same monorepo.
228
438
  */
229
439
  export function updatePlugin(name: string): void {
230
440
  const targetDir = path.join(PLUGINS_DIR, name);
@@ -232,6 +442,53 @@ export function updatePlugin(name: string): void {
232
442
  throw new Error(`Plugin "${name}" is not installed.`);
233
443
  }
234
444
 
445
+ const lock = readLockFile();
446
+ const lockEntry = lock[name];
447
+
448
+ if (lockEntry?.monorepo) {
449
+ // Monorepo update: pull the repo root
450
+ const monoDir = path.join(getMonoreposDir(), lockEntry.monorepo.name);
451
+ try {
452
+ execFileSync('git', ['pull', '--ff-only'], {
453
+ cwd: monoDir,
454
+ encoding: 'utf-8',
455
+ stdio: ['pipe', 'pipe', 'pipe'],
456
+ });
457
+ } catch (err) {
458
+ throw new Error(`Failed to update monorepo: ${getErrorMessage(err)}`);
459
+ }
460
+
461
+ // Re-run lifecycle for ALL sub-plugins from this monorepo
462
+ const monoName = lockEntry.monorepo.name;
463
+ const commitHash = getCommitHash(monoDir);
464
+ const pluginDirs: string[] = [];
465
+ for (const [pluginName, entry] of Object.entries(lock)) {
466
+ if (entry.monorepo?.name !== monoName) continue;
467
+ const subDir = path.join(monoDir, entry.monorepo.subPath);
468
+ const validation = validatePluginStructure(subDir);
469
+ if (!validation.valid) {
470
+ log.warn(`Plugin "${pluginName}" structure invalid after update:\n- ${validation.errors.join('\n- ')}`);
471
+ }
472
+ pluginDirs.push(subDir);
473
+ }
474
+ if (pluginDirs.length > 0) {
475
+ postInstallMonorepoLifecycle(monoDir, pluginDirs);
476
+ }
477
+ for (const [pluginName, entry] of Object.entries(lock)) {
478
+ if (entry.monorepo?.name !== monoName) continue;
479
+ if (commitHash) {
480
+ lock[pluginName] = {
481
+ ...entry,
482
+ commitHash,
483
+ updatedAt: new Date().toISOString(),
484
+ };
485
+ }
486
+ }
487
+ writeLockFile(lock);
488
+ return;
489
+ }
490
+
491
+ // Standard single-plugin update
235
492
  try {
236
493
  execFileSync('git', ['pull', '--ff-only'], {
237
494
  cwd: targetDir,
@@ -251,7 +508,6 @@ export function updatePlugin(name: string): void {
251
508
 
252
509
  const commitHash = getCommitHash(targetDir);
253
510
  if (commitHash) {
254
- const lock = readLockFile();
255
511
  const existing = lock[name];
256
512
  lock[name] = {
257
513
  source: existing?.source ?? getPluginSource(targetDir) ?? '',
@@ -290,6 +546,7 @@ export function updateAllPlugins(): UpdateResult[] {
290
546
 
291
547
  /**
292
548
  * List all installed plugins.
549
+ * Reads opencli-plugin.json for description/version when available.
293
550
  */
294
551
  export function listPlugins(): PluginInfo[] {
295
552
  if (!fs.existsSync(PLUGINS_DIR)) return [];
@@ -299,19 +556,42 @@ export function listPlugins(): PluginInfo[] {
299
556
  const plugins: PluginInfo[] = [];
300
557
 
301
558
  for (const entry of entries) {
302
- if (!entry.isDirectory()) continue;
559
+ // Accept both real directories and symlinks (monorepo sub-plugins)
303
560
  const pluginDir = path.join(PLUGINS_DIR, entry.name);
561
+ const isDir = entry.isDirectory() || isSymlinkSync(pluginDir);
562
+ if (!isDir) continue;
563
+
304
564
  const commands = scanPluginCommands(pluginDir);
305
- const source = getPluginSource(pluginDir);
306
565
  const lockEntry = lock[entry.name];
307
566
 
567
+ // Try to read manifest for metadata
568
+ const manifest = readPluginManifest(pluginDir);
569
+ // For monorepo sub-plugins, also check the monorepo root manifest
570
+ let description = manifest?.description;
571
+ let version = manifest?.version;
572
+ if (lockEntry?.monorepo && !description) {
573
+ const monoDir = path.join(getMonoreposDir(), lockEntry.monorepo.name);
574
+ const monoManifest = readPluginManifest(monoDir);
575
+ const subEntry = monoManifest?.plugins?.[entry.name];
576
+ if (subEntry) {
577
+ description = description ?? subEntry.description;
578
+ version = version ?? subEntry.version;
579
+ }
580
+ }
581
+
582
+ const source = lockEntry?.monorepo
583
+ ? lockEntry.source
584
+ : getPluginSource(pluginDir);
585
+
308
586
  plugins.push({
309
587
  name: entry.name,
310
588
  path: pluginDir,
311
589
  commands,
312
590
  source,
313
- version: lockEntry?.commitHash?.slice(0, 7),
591
+ version: version ?? lockEntry?.commitHash?.slice(0, 7),
314
592
  installedAt: lockEntry?.installedAt,
593
+ monorepoName: lockEntry?.monorepo?.name,
594
+ description,
315
595
  });
316
596
  }
317
597
 
@@ -350,8 +630,24 @@ function getPluginSource(dir: string): string | undefined {
350
630
  }
351
631
  }
352
632
 
353
- /** Parse a plugin source string into clone URL and name */
354
- function parseSource(source: string): { cloneUrl: string; name: string } | null {
633
+ /** Parse a plugin source string into clone URL, repo name, and optional sub-plugin. */
634
+ function parseSource(
635
+ source: string,
636
+ ): { cloneUrl: string; name: string; subPlugin?: string } | null {
637
+ // github:user/repo/subplugin (monorepo specific sub-plugin)
638
+ const githubSubMatch = source.match(
639
+ /^github:([\w.-]+)\/([\w.-]+)\/([\w.-]+)$/,
640
+ );
641
+ if (githubSubMatch) {
642
+ const [, user, repo, sub] = githubSubMatch;
643
+ const name = repo.replace(/^opencli-plugin-/, '');
644
+ return {
645
+ cloneUrl: `https://github.com/${user}/${repo}.git`,
646
+ name,
647
+ subPlugin: sub,
648
+ };
649
+ }
650
+
355
651
  // github:user/repo
356
652
  const githubMatch = source.match(/^github:([\w.-]+)\/([\w.-]+)$/);
357
653
  if (githubMatch) {
@@ -364,7 +660,9 @@ function parseSource(source: string): { cloneUrl: string; name: string } | null
364
660
  }
365
661
 
366
662
  // https://github.com/user/repo (or .git)
367
- const urlMatch = source.match(/^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
663
+ const urlMatch = source.match(
664
+ /^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+?)(?:\.git)?$/,
665
+ );
368
666
  if (urlMatch) {
369
667
  const [, user, repo] = urlMatch;
370
668
  const name = repo.replace(/^opencli-plugin-/, '');
@@ -514,9 +812,13 @@ function transpilePluginTs(pluginDir: string): void {
514
812
  export {
515
813
  resolveEsbuildBin as _resolveEsbuildBin,
516
814
  getCommitHash as _getCommitHash,
815
+ installDependencies as _installDependencies,
517
816
  parseSource as _parseSource,
817
+ postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle,
518
818
  readLockFile as _readLockFile,
519
819
  updateAllPlugins as _updateAllPlugins,
520
820
  validatePluginStructure as _validatePluginStructure,
521
821
  writeLockFile as _writeLockFile,
822
+ isSymlinkSync as _isSymlinkSync,
823
+ getMonoreposDir as _getMonoreposDir,
522
824
  };
package/src/registry.ts CHANGED
@@ -46,6 +46,10 @@ export interface CliCommand {
46
46
  source?: string;
47
47
  footerExtra?: (kwargs: CommandArgs) => string | undefined;
48
48
  requiredEnv?: RequiredEnv[];
49
+ /** Deprecation note shown in help / execution warnings. */
50
+ deprecated?: boolean | string;
51
+ /** Preferred replacement command, if any. */
52
+ replacedBy?: string;
49
53
  /**
50
54
  * Control pre-navigation for cookie/header context before command execution.
51
55
  *
@@ -95,6 +99,8 @@ export function cli(opts: CliOptions): CliCommand {
95
99
  timeoutSeconds: opts.timeoutSeconds,
96
100
  footerExtra: opts.footerExtra,
97
101
  requiredEnv: opts.requiredEnv,
102
+ deprecated: opts.deprecated,
103
+ replacedBy: opts.replacedBy,
98
104
  navigateBefore: opts.navigateBefore,
99
105
  };
100
106
 
@@ -118,4 +124,3 @@ export function strategyLabel(cmd: CliCommand): string {
118
124
  export function registerCommand(cmd: CliCommand): void {
119
125
  _registry.set(fullName(cmd), cmd);
120
126
  }
121
-
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { detectRuntime, getRuntimeVersion, getRuntimeLabel } from './runtime-detect.js';
3
+
4
+ describe('runtime-detect', () => {
5
+ it('detectRuntime returns a valid runtime string', () => {
6
+ const rt = detectRuntime();
7
+ expect(['bun', 'node']).toContain(rt);
8
+ });
9
+
10
+ it('getRuntimeVersion returns a non-empty version string', () => {
11
+ const ver = getRuntimeVersion();
12
+ expect(typeof ver).toBe('string');
13
+ expect(ver.length).toBeGreaterThan(0);
14
+ });
15
+
16
+ it('getRuntimeLabel returns "<runtime> <version>" format', () => {
17
+ const label = getRuntimeLabel();
18
+ expect(label).toMatch(/^(bun|node) .+$/);
19
+ });
20
+
21
+ it('detects the current environment correctly', () => {
22
+ const isBun = typeof (globalThis as any).Bun !== 'undefined';
23
+ const rt = detectRuntime();
24
+ if (isBun) {
25
+ expect(rt).toBe('bun');
26
+ } else {
27
+ expect(rt).toBe('node');
28
+ }
29
+ });
30
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Runtime detection — identify whether opencli is running under Node.js or Bun.
3
+ *
4
+ * Bun injects `globalThis.Bun` at startup, making detection trivial.
5
+ * This module centralises the check so other code can adapt behaviour
6
+ * (e.g. logging, diagnostics) without littering runtime sniffing everywhere.
7
+ */
8
+
9
+ export type Runtime = 'bun' | 'node';
10
+
11
+ /**
12
+ * Detect the current JavaScript runtime.
13
+ */
14
+ export function detectRuntime(): Runtime {
15
+ // Bun always exposes globalThis.Bun (including Bun.version)
16
+ if (typeof (globalThis as any).Bun !== 'undefined') return 'bun';
17
+ return 'node';
18
+ }
19
+
20
+ /**
21
+ * Return a human-readable version string for the current runtime.
22
+ * Examples: "v22.13.0" (Node), "1.1.42" (Bun)
23
+ */
24
+ export function getRuntimeVersion(): string {
25
+ if (detectRuntime() === 'bun') {
26
+ return (globalThis as any).Bun.version as string;
27
+ }
28
+ return process.version; // e.g. "v22.13.0"
29
+ }
30
+
31
+ /**
32
+ * Return a combined label like "node v22.13.0" or "bun 1.1.42".
33
+ */
34
+ export function getRuntimeLabel(): string {
35
+ return `${detectRuntime()} ${getRuntimeVersion()}`;
36
+ }
package/src/runtime.ts CHANGED
@@ -7,7 +7,7 @@ import { TimeoutError } from './errors.js';
7
7
  * Uses CDPBridge when OPENCLI_CDP_ENDPOINT is set, otherwise BrowserBridge.
8
8
  */
9
9
  export function getBrowserFactory(): new () => IBrowserFactory {
10
- return (process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge) as unknown as new () => IBrowserFactory;
10
+ return process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
11
11
  }
12
12
 
13
13
  function parseEnvTimeout(envVar: string, fallback: number): number {
@@ -45,6 +45,8 @@ export function serializeCommand(cmd: CliCommand) {
45
45
  args: cmd.args.map(serializeArg),
46
46
  columns: cmd.columns ?? [],
47
47
  domain: cmd.domain ?? null,
48
+ deprecated: cmd.deprecated ?? null,
49
+ replacedBy: cmd.replacedBy ?? null,
48
50
  };
49
51
  }
50
52
 
@@ -73,6 +75,8 @@ export function formatRegistryHelpText(cmd: CliCommand): string {
73
75
  meta.push(`Strategy: ${strategyLabel(cmd)}`);
74
76
  meta.push(`Browser: ${cmd.browser ? 'yes' : 'no'}`);
75
77
  if (cmd.domain) meta.push(`Domain: ${cmd.domain}`);
78
+ if (cmd.deprecated) meta.push(`Deprecated: ${typeof cmd.deprecated === 'string' ? cmd.deprecated : 'yes'}`);
79
+ if (cmd.replacedBy) meta.push(`Use instead: ${cmd.replacedBy}`);
76
80
  lines.push(meta.join(' | '));
77
81
  if (cmd.columns?.length) lines.push(`Output columns: ${cmd.columns.join(', ')}`);
78
82
  return '\n' + lines.join('\n') + '\n';
package/src/types.ts CHANGED
@@ -65,4 +65,5 @@ export interface IPage {
65
65
  installInterceptor(pattern: string): Promise<void>;
66
66
  getInterceptedRequests(): Promise<any[]>;
67
67
  screenshot(options?: ScreenshotOptions): Promise<string>;
68
+ closeWindow?(): Promise<void>;
68
69
  }