@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/dist/plugin.js 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
  import * as fs from 'node:fs';
8
9
  import * as os from 'node:os';
@@ -12,6 +13,7 @@ import { fileURLToPath } from 'node:url';
12
13
  import { PLUGINS_DIR } from './discovery.js';
13
14
  import { getErrorMessage } from './errors.js';
14
15
  import { log } from './logger.js';
16
+ import { readPluginManifest, isMonorepo, getEnabledPlugins, checkCompatibility, } from './plugin-manifest.js';
15
17
  const isWindows = process.platform === 'win32';
16
18
  /** Get home directory, respecting HOME environment variable for test isolation. */
17
19
  function getHomeDir() {
@@ -21,8 +23,13 @@ function getHomeDir() {
21
23
  export function getLockFilePath() {
22
24
  return path.join(getHomeDir(), '.opencli', 'plugins.lock.json');
23
25
  }
26
+ /** Monorepo clones directory: ~/.opencli/monorepos/ */
27
+ export function getMonoreposDir() {
28
+ return path.join(getHomeDir(), '.opencli', 'monorepos');
29
+ }
24
30
  // Legacy const for backward compatibility (computed at load time)
25
31
  export const LOCK_FILE = path.join(os.homedir(), '.opencli', 'plugins.lock.json');
32
+ export const MONOREPOS_DIR = path.join(os.homedir(), '.opencli', 'monorepos');
26
33
  // ── Lock file helpers ───────────────────────────────────────────────────────
27
34
  export function readLockFile() {
28
35
  try {
@@ -87,34 +94,53 @@ export function validatePluginStructure(pluginDir) {
87
94
  }
88
95
  return { valid: errors.length === 0, errors };
89
96
  }
90
- /**
91
- * Shared post-install lifecycle: npm install → host symlink → TS transpile.
92
- * Called by both installPlugin() and updatePlugin().
93
- */
94
- function postInstallLifecycle(pluginDir) {
95
- const pkgJsonPath = path.join(pluginDir, 'package.json');
97
+ function installDependencies(dir) {
98
+ const pkgJsonPath = path.join(dir, 'package.json');
96
99
  if (!fs.existsSync(pkgJsonPath))
97
100
  return;
98
101
  try {
99
102
  execFileSync('npm', ['install', '--omit=dev'], {
100
- cwd: pluginDir,
103
+ cwd: dir,
101
104
  encoding: 'utf-8',
102
105
  stdio: ['pipe', 'pipe', 'pipe'],
103
106
  ...(isWindows && { shell: true }),
104
107
  });
105
108
  }
106
109
  catch (err) {
107
- console.error(`[plugin] npm install failed in ${pluginDir}: ${err instanceof Error ? err.message : err}`);
110
+ throw new Error(`npm install failed in ${dir}: ${getErrorMessage(err)}`);
108
111
  }
112
+ }
113
+ function finalizePluginRuntime(pluginDir) {
109
114
  // Symlink host opencli so TS plugins resolve '@jackwener/opencli/registry'
110
115
  // against the running host, not a stale npm-published version.
111
116
  linkHostOpencli(pluginDir);
112
117
  // Transpile .ts → .js via esbuild (production node can't load .ts directly).
113
118
  transpilePluginTs(pluginDir);
114
119
  }
120
+ /**
121
+ * Shared post-install lifecycle for standalone plugins.
122
+ */
123
+ function postInstallLifecycle(pluginDir) {
124
+ installDependencies(pluginDir);
125
+ finalizePluginRuntime(pluginDir);
126
+ }
127
+ /**
128
+ * Monorepo lifecycle: install shared deps once at repo root, then finalize each sub-plugin.
129
+ */
130
+ function postInstallMonorepoLifecycle(repoDir, pluginDirs) {
131
+ installDependencies(repoDir);
132
+ for (const pluginDir of pluginDirs) {
133
+ finalizePluginRuntime(pluginDir);
134
+ }
135
+ }
115
136
  /**
116
137
  * Install a plugin from a source.
117
- * Currently supports "github:user/repo" format (git clone wrapper).
138
+ * Supports:
139
+ * "github:user/repo" — single plugin or full monorepo
140
+ * "github:user/repo/subplugin" — specific sub-plugin from a monorepo
141
+ * "https://github.com/user/repo"
142
+ *
143
+ * Returns the installed plugin name(s).
118
144
  */
119
145
  export function installPlugin(source) {
120
146
  const parsed = parseSource(source);
@@ -122,17 +148,14 @@ export function installPlugin(source) {
122
148
  throw new Error(`Invalid plugin source: "${source}"\n` +
123
149
  `Supported formats:\n` +
124
150
  ` github:user/repo\n` +
151
+ ` github:user/repo/subplugin\n` +
125
152
  ` https://github.com/user/repo`);
126
153
  }
127
- const { cloneUrl, name } = parsed;
128
- const targetDir = path.join(PLUGINS_DIR, name);
129
- if (fs.existsSync(targetDir)) {
130
- throw new Error(`Plugin "${name}" is already installed at ${targetDir}`);
131
- }
132
- // Ensure plugins directory exists
133
- fs.mkdirSync(PLUGINS_DIR, { recursive: true });
154
+ const { cloneUrl, name: repoName, subPlugin } = parsed;
155
+ // Clone to a temporary location first so we can inspect the manifest
156
+ const tmpCloneDir = path.join(os.tmpdir(), `opencli-clone-${Date.now()}`);
134
157
  try {
135
- execFileSync('git', ['clone', '--depth', '1', cloneUrl, targetDir], {
158
+ execFileSync('git', ['clone', '--depth', '1', cloneUrl, tmpCloneDir], {
136
159
  encoding: 'utf-8',
137
160
  stdio: ['pipe', 'pipe', 'pipe'],
138
161
  });
@@ -140,48 +163,228 @@ export function installPlugin(source) {
140
163
  catch (err) {
141
164
  throw new Error(`Failed to clone plugin: ${getErrorMessage(err)}`);
142
165
  }
143
- const validation = validatePluginStructure(targetDir);
166
+ try {
167
+ const manifest = readPluginManifest(tmpCloneDir);
168
+ // Check top-level compatibility
169
+ if (manifest?.opencli && !checkCompatibility(manifest.opencli)) {
170
+ throw new Error(`Plugin requires opencli ${manifest.opencli}, but current version is incompatible.`);
171
+ }
172
+ if (manifest && isMonorepo(manifest)) {
173
+ return installMonorepo(tmpCloneDir, cloneUrl, repoName, manifest, subPlugin);
174
+ }
175
+ // Single plugin mode
176
+ return installSinglePlugin(tmpCloneDir, cloneUrl, repoName, manifest);
177
+ }
178
+ finally {
179
+ // Clean up temp clone (may already have been moved)
180
+ try {
181
+ fs.rmSync(tmpCloneDir, { recursive: true, force: true });
182
+ }
183
+ catch { }
184
+ }
185
+ }
186
+ /** Install a single (non-monorepo) plugin. */
187
+ function installSinglePlugin(cloneDir, cloneUrl, name, manifest) {
188
+ const pluginName = manifest?.name ?? name;
189
+ const targetDir = path.join(PLUGINS_DIR, pluginName);
190
+ if (fs.existsSync(targetDir)) {
191
+ throw new Error(`Plugin "${pluginName}" is already installed at ${targetDir}`);
192
+ }
193
+ const validation = validatePluginStructure(cloneDir);
144
194
  if (!validation.valid) {
145
- // If validation fails, clean up the cloned directory and abort
146
- fs.rmSync(targetDir, { recursive: true, force: true });
147
195
  throw new Error(`Invalid plugin structure:\n- ${validation.errors.join('\n- ')}`);
148
196
  }
197
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
198
+ fs.renameSync(cloneDir, targetDir);
149
199
  postInstallLifecycle(targetDir);
150
200
  const commitHash = getCommitHash(targetDir);
151
201
  if (commitHash) {
152
202
  const lock = readLockFile();
153
- lock[name] = {
203
+ lock[pluginName] = {
154
204
  source: cloneUrl,
155
205
  commitHash,
156
206
  installedAt: new Date().toISOString(),
157
207
  };
158
208
  writeLockFile(lock);
159
209
  }
160
- return name;
210
+ return pluginName;
211
+ }
212
+ /** Install sub-plugins from a monorepo. */
213
+ function installMonorepo(cloneDir, cloneUrl, repoName, manifest, subPlugin) {
214
+ const monoreposDir = getMonoreposDir();
215
+ const repoDir = path.join(monoreposDir, repoName);
216
+ // Move clone to permanent monorepos location (if not already there)
217
+ if (!fs.existsSync(repoDir)) {
218
+ fs.mkdirSync(monoreposDir, { recursive: true });
219
+ fs.renameSync(cloneDir, repoDir);
220
+ }
221
+ let pluginsToInstall = getEnabledPlugins(manifest);
222
+ // If a specific sub-plugin was requested, filter to just that one
223
+ if (subPlugin) {
224
+ pluginsToInstall = pluginsToInstall.filter((p) => p.name === subPlugin);
225
+ if (pluginsToInstall.length === 0) {
226
+ // Check if it exists but is disabled
227
+ const disabled = manifest.plugins?.[subPlugin];
228
+ if (disabled) {
229
+ throw new Error(`Sub-plugin "${subPlugin}" is disabled in the manifest.`);
230
+ }
231
+ throw new Error(`Sub-plugin "${subPlugin}" not found in monorepo. Available: ${Object.keys(manifest.plugins ?? {}).join(', ')}`);
232
+ }
233
+ }
234
+ const installedNames = [];
235
+ const lock = readLockFile();
236
+ const commitHash = getCommitHash(repoDir);
237
+ const eligiblePlugins = [];
238
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
239
+ for (const { name, entry } of pluginsToInstall) {
240
+ // Check sub-plugin level compatibility (overrides top-level)
241
+ if (entry.opencli && !checkCompatibility(entry.opencli)) {
242
+ log.warn(`Skipping "${name}": requires opencli ${entry.opencli}`);
243
+ continue;
244
+ }
245
+ const subDir = path.join(repoDir, entry.path);
246
+ if (!fs.existsSync(subDir)) {
247
+ log.warn(`Skipping "${name}": path "${entry.path}" not found in repo.`);
248
+ continue;
249
+ }
250
+ const validation = validatePluginStructure(subDir);
251
+ if (!validation.valid) {
252
+ log.warn(`Skipping "${name}": invalid structure — ${validation.errors.join(', ')}`);
253
+ continue;
254
+ }
255
+ const linkPath = path.join(PLUGINS_DIR, name);
256
+ if (fs.existsSync(linkPath)) {
257
+ log.warn(`Skipping "${name}": already installed at ${linkPath}`);
258
+ continue;
259
+ }
260
+ eligiblePlugins.push({ name, entry, subDir });
261
+ }
262
+ if (eligiblePlugins.length > 0) {
263
+ postInstallMonorepoLifecycle(repoDir, eligiblePlugins.map((p) => p.subDir));
264
+ }
265
+ for (const { name, entry, subDir } of eligiblePlugins) {
266
+ const linkPath = path.join(PLUGINS_DIR, name);
267
+ // Create symlink (junction on Windows)
268
+ const linkType = isWindows ? 'junction' : 'dir';
269
+ fs.symlinkSync(subDir, linkPath, linkType);
270
+ if (commitHash) {
271
+ lock[name] = {
272
+ source: cloneUrl,
273
+ commitHash,
274
+ installedAt: new Date().toISOString(),
275
+ monorepo: { name: repoName, subPath: entry.path },
276
+ };
277
+ }
278
+ installedNames.push(name);
279
+ }
280
+ writeLockFile(lock);
281
+ return installedNames;
161
282
  }
162
283
  /**
163
284
  * Uninstall a plugin by name.
285
+ * For monorepo sub-plugins: removes symlink and cleans up the monorepo
286
+ * directory when no more sub-plugins reference it.
164
287
  */
165
288
  export function uninstallPlugin(name) {
166
289
  const targetDir = path.join(PLUGINS_DIR, name);
167
290
  if (!fs.existsSync(targetDir)) {
168
291
  throw new Error(`Plugin "${name}" is not installed.`);
169
292
  }
170
- fs.rmSync(targetDir, { recursive: true, force: true });
171
293
  const lock = readLockFile();
172
- if (lock[name]) {
294
+ const lockEntry = lock[name];
295
+ // Check if this is a symlink (monorepo sub-plugin)
296
+ const isSymlink = isSymlinkSync(targetDir);
297
+ if (isSymlink) {
298
+ // Remove symlink only (not the actual directory)
299
+ fs.unlinkSync(targetDir);
300
+ }
301
+ else {
302
+ fs.rmSync(targetDir, { recursive: true, force: true });
303
+ }
304
+ // Clean up monorepo directory if no more sub-plugins reference it
305
+ if (lockEntry?.monorepo) {
306
+ delete lock[name];
307
+ const monoName = lockEntry.monorepo.name;
308
+ const stillReferenced = Object.values(lock).some((entry) => entry.monorepo?.name === monoName);
309
+ if (!stillReferenced) {
310
+ const monoDir = path.join(getMonoreposDir(), monoName);
311
+ try {
312
+ fs.rmSync(monoDir, { recursive: true, force: true });
313
+ }
314
+ catch { }
315
+ }
316
+ }
317
+ else if (lock[name]) {
173
318
  delete lock[name];
174
- writeLockFile(lock);
319
+ }
320
+ writeLockFile(lock);
321
+ }
322
+ /** Synchronous check if a path is a symlink. */
323
+ function isSymlinkSync(p) {
324
+ try {
325
+ return fs.lstatSync(p).isSymbolicLink();
326
+ }
327
+ catch {
328
+ return false;
175
329
  }
176
330
  }
177
331
  /**
178
332
  * Update a plugin by name (git pull + re-install lifecycle).
333
+ * For monorepo sub-plugins: pulls the monorepo root and re-runs lifecycle
334
+ * for all sub-plugins from the same monorepo.
179
335
  */
180
336
  export function updatePlugin(name) {
181
337
  const targetDir = path.join(PLUGINS_DIR, name);
182
338
  if (!fs.existsSync(targetDir)) {
183
339
  throw new Error(`Plugin "${name}" is not installed.`);
184
340
  }
341
+ const lock = readLockFile();
342
+ const lockEntry = lock[name];
343
+ if (lockEntry?.monorepo) {
344
+ // Monorepo update: pull the repo root
345
+ const monoDir = path.join(getMonoreposDir(), lockEntry.monorepo.name);
346
+ try {
347
+ execFileSync('git', ['pull', '--ff-only'], {
348
+ cwd: monoDir,
349
+ encoding: 'utf-8',
350
+ stdio: ['pipe', 'pipe', 'pipe'],
351
+ });
352
+ }
353
+ catch (err) {
354
+ throw new Error(`Failed to update monorepo: ${getErrorMessage(err)}`);
355
+ }
356
+ // Re-run lifecycle for ALL sub-plugins from this monorepo
357
+ const monoName = lockEntry.monorepo.name;
358
+ const commitHash = getCommitHash(monoDir);
359
+ const pluginDirs = [];
360
+ for (const [pluginName, entry] of Object.entries(lock)) {
361
+ if (entry.monorepo?.name !== monoName)
362
+ continue;
363
+ const subDir = path.join(monoDir, entry.monorepo.subPath);
364
+ const validation = validatePluginStructure(subDir);
365
+ if (!validation.valid) {
366
+ log.warn(`Plugin "${pluginName}" structure invalid after update:\n- ${validation.errors.join('\n- ')}`);
367
+ }
368
+ pluginDirs.push(subDir);
369
+ }
370
+ if (pluginDirs.length > 0) {
371
+ postInstallMonorepoLifecycle(monoDir, pluginDirs);
372
+ }
373
+ for (const [pluginName, entry] of Object.entries(lock)) {
374
+ if (entry.monorepo?.name !== monoName)
375
+ continue;
376
+ if (commitHash) {
377
+ lock[pluginName] = {
378
+ ...entry,
379
+ commitHash,
380
+ updatedAt: new Date().toISOString(),
381
+ };
382
+ }
383
+ }
384
+ writeLockFile(lock);
385
+ return;
386
+ }
387
+ // Standard single-plugin update
185
388
  try {
186
389
  execFileSync('git', ['pull', '--ff-only'], {
187
390
  cwd: targetDir,
@@ -199,7 +402,6 @@ export function updatePlugin(name) {
199
402
  postInstallLifecycle(targetDir);
200
403
  const commitHash = getCommitHash(targetDir);
201
404
  if (commitHash) {
202
- const lock = readLockFile();
203
405
  const existing = lock[name];
204
406
  lock[name] = {
205
407
  source: existing?.source ?? getPluginSource(targetDir) ?? '',
@@ -231,6 +433,7 @@ export function updateAllPlugins() {
231
433
  }
232
434
  /**
233
435
  * List all installed plugins.
436
+ * Reads opencli-plugin.json for description/version when available.
234
437
  */
235
438
  export function listPlugins() {
236
439
  if (!fs.existsSync(PLUGINS_DIR))
@@ -239,19 +442,39 @@ export function listPlugins() {
239
442
  const lock = readLockFile();
240
443
  const plugins = [];
241
444
  for (const entry of entries) {
242
- if (!entry.isDirectory())
243
- continue;
445
+ // Accept both real directories and symlinks (monorepo sub-plugins)
244
446
  const pluginDir = path.join(PLUGINS_DIR, entry.name);
447
+ const isDir = entry.isDirectory() || isSymlinkSync(pluginDir);
448
+ if (!isDir)
449
+ continue;
245
450
  const commands = scanPluginCommands(pluginDir);
246
- const source = getPluginSource(pluginDir);
247
451
  const lockEntry = lock[entry.name];
452
+ // Try to read manifest for metadata
453
+ const manifest = readPluginManifest(pluginDir);
454
+ // For monorepo sub-plugins, also check the monorepo root manifest
455
+ let description = manifest?.description;
456
+ let version = manifest?.version;
457
+ if (lockEntry?.monorepo && !description) {
458
+ const monoDir = path.join(getMonoreposDir(), lockEntry.monorepo.name);
459
+ const monoManifest = readPluginManifest(monoDir);
460
+ const subEntry = monoManifest?.plugins?.[entry.name];
461
+ if (subEntry) {
462
+ description = description ?? subEntry.description;
463
+ version = version ?? subEntry.version;
464
+ }
465
+ }
466
+ const source = lockEntry?.monorepo
467
+ ? lockEntry.source
468
+ : getPluginSource(pluginDir);
248
469
  plugins.push({
249
470
  name: entry.name,
250
471
  path: pluginDir,
251
472
  commands,
252
473
  source,
253
- version: lockEntry?.commitHash?.slice(0, 7),
474
+ version: version ?? lockEntry?.commitHash?.slice(0, 7),
254
475
  installedAt: lockEntry?.installedAt,
476
+ monorepoName: lockEntry?.monorepo?.name,
477
+ description,
255
478
  });
256
479
  }
257
480
  return plugins;
@@ -284,8 +507,19 @@ function getPluginSource(dir) {
284
507
  return undefined;
285
508
  }
286
509
  }
287
- /** Parse a plugin source string into clone URL and name */
510
+ /** Parse a plugin source string into clone URL, repo name, and optional sub-plugin. */
288
511
  function parseSource(source) {
512
+ // github:user/repo/subplugin (monorepo specific sub-plugin)
513
+ const githubSubMatch = source.match(/^github:([\w.-]+)\/([\w.-]+)\/([\w.-]+)$/);
514
+ if (githubSubMatch) {
515
+ const [, user, repo, sub] = githubSubMatch;
516
+ const name = repo.replace(/^opencli-plugin-/, '');
517
+ return {
518
+ cloneUrl: `https://github.com/${user}/${repo}.git`,
519
+ name,
520
+ subPlugin: sub,
521
+ };
522
+ }
289
523
  // github:user/repo
290
524
  const githubMatch = source.match(/^github:([\w.-]+)\/([\w.-]+)$/);
291
525
  if (githubMatch) {
@@ -432,4 +666,4 @@ function transpilePluginTs(pluginDir) {
432
666
  // Non-fatal: skip transpilation if anything goes wrong
433
667
  }
434
668
  }
435
- export { resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, parseSource as _parseSource, readLockFile as _readLockFile, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, };
669
+ export { resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, };
@@ -7,7 +7,11 @@ import * as os from 'node:os';
7
7
  import * as path from 'node:path';
8
8
  import { PLUGINS_DIR } from './discovery.js';
9
9
  import * as pluginModule from './plugin.js';
10
- const { LOCK_FILE, _getCommitHash, listPlugins, _readLockFile, _resolveEsbuildBin, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, } = pluginModule;
10
+ const { mockExecFileSync, mockExecSync } = vi.hoisted(() => ({
11
+ mockExecFileSync: vi.fn(),
12
+ mockExecSync: vi.fn(),
13
+ }));
14
+ const { LOCK_FILE, _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, listPlugins, _readLockFile, _resolveEsbuildBin, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _isSymlinkSync, _getMonoreposDir, } = pluginModule;
11
15
  describe('parseSource', () => {
12
16
  it('parses github:user/repo format', () => {
13
17
  const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
@@ -239,7 +243,7 @@ describe('updatePlugin', () => {
239
243
  });
240
244
  vi.mock('node:child_process', () => {
241
245
  return {
242
- execFileSync: vi.fn((_cmd, args, opts) => {
246
+ execFileSync: mockExecFileSync.mockImplementation((_cmd, args, opts) => {
243
247
  if (Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
244
248
  if (opts?.cwd === os.tmpdir()) {
245
249
  throw new Error('not a git repository');
@@ -251,9 +255,50 @@ vi.mock('node:child_process', () => {
251
255
  }
252
256
  return '';
253
257
  }),
254
- execSync: vi.fn(() => ''),
258
+ execSync: mockExecSync.mockImplementation(() => ''),
255
259
  };
256
260
  });
261
+ describe('installDependencies', () => {
262
+ beforeEach(() => {
263
+ mockExecFileSync.mockClear();
264
+ mockExecSync.mockClear();
265
+ });
266
+ it('throws when npm install fails', () => {
267
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-plugin-b-'));
268
+ const failingDir = path.join(tmpDir, 'plugin-b');
269
+ fs.mkdirSync(failingDir, { recursive: true });
270
+ fs.writeFileSync(path.join(failingDir, 'package.json'), JSON.stringify({ name: 'plugin-b' }));
271
+ expect(() => _installDependencies(failingDir)).toThrow('npm install failed');
272
+ fs.rmSync(tmpDir, { recursive: true, force: true });
273
+ });
274
+ });
275
+ describe('postInstallMonorepoLifecycle', () => {
276
+ let repoDir;
277
+ let subDir;
278
+ beforeEach(() => {
279
+ mockExecFileSync.mockClear();
280
+ mockExecSync.mockClear();
281
+ repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-monorepo-'));
282
+ subDir = path.join(repoDir, 'packages', 'alpha');
283
+ fs.mkdirSync(subDir, { recursive: true });
284
+ fs.writeFileSync(path.join(repoDir, 'package.json'), JSON.stringify({
285
+ name: 'opencli-plugins',
286
+ private: true,
287
+ workspaces: ['packages/*'],
288
+ }));
289
+ fs.writeFileSync(path.join(subDir, 'hello.yaml'), 'site: test\nname: hello\n');
290
+ });
291
+ afterEach(() => {
292
+ fs.rmSync(repoDir, { recursive: true, force: true });
293
+ });
294
+ it('installs dependencies once at the monorepo root, not in each sub-plugin', () => {
295
+ _postInstallMonorepoLifecycle(repoDir, [subDir]);
296
+ const npmCalls = mockExecFileSync.mock.calls.filter(([cmd, args]) => cmd === 'npm' && Array.isArray(args) && args[0] === 'install');
297
+ expect(npmCalls).toHaveLength(1);
298
+ expect(npmCalls[0][2]).toMatchObject({ cwd: repoDir });
299
+ expect(npmCalls.some(([, , opts]) => opts?.cwd === subDir)).toBe(false);
300
+ });
301
+ });
257
302
  describe('updateAllPlugins', () => {
258
303
  const testDirA = path.join(PLUGINS_DIR, 'plugin-a');
259
304
  const testDirB = path.join(PLUGINS_DIR, 'plugin-b');
@@ -295,3 +340,175 @@ describe('updateAllPlugins', () => {
295
340
  expect(resC.success).toBe(true);
296
341
  });
297
342
  });
343
+ // ── Monorepo-specific tests ─────────────────────────────────────────────────
344
+ describe('parseSource with monorepo subplugin', () => {
345
+ it('parses github:user/repo/subplugin format', () => {
346
+ const result = _parseSource('github:ByteYue/opencli-plugins/polymarket');
347
+ expect(result).toEqual({
348
+ cloneUrl: 'https://github.com/ByteYue/opencli-plugins.git',
349
+ name: 'opencli-plugins',
350
+ subPlugin: 'polymarket',
351
+ });
352
+ });
353
+ it('strips opencli-plugin- prefix from repo name in subplugin format', () => {
354
+ const result = _parseSource('github:user/opencli-plugin-collection/defi');
355
+ expect(result.name).toBe('collection');
356
+ expect(result.subPlugin).toBe('defi');
357
+ });
358
+ it('still parses github:user/repo without subplugin', () => {
359
+ const result = _parseSource('github:user/my-repo');
360
+ expect(result).toEqual({
361
+ cloneUrl: 'https://github.com/user/my-repo.git',
362
+ name: 'my-repo',
363
+ });
364
+ expect(result.subPlugin).toBeUndefined();
365
+ });
366
+ });
367
+ describe('isSymlinkSync', () => {
368
+ let tmpDir;
369
+ beforeEach(() => {
370
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-symlink-test-'));
371
+ });
372
+ afterEach(() => {
373
+ fs.rmSync(tmpDir, { recursive: true, force: true });
374
+ });
375
+ it('returns false for a regular directory', () => {
376
+ const dir = path.join(tmpDir, 'regular');
377
+ fs.mkdirSync(dir);
378
+ expect(_isSymlinkSync(dir)).toBe(false);
379
+ });
380
+ it('returns true for a symlink', () => {
381
+ const target = path.join(tmpDir, 'target');
382
+ const link = path.join(tmpDir, 'link');
383
+ fs.mkdirSync(target);
384
+ fs.symlinkSync(target, link, 'dir');
385
+ expect(_isSymlinkSync(link)).toBe(true);
386
+ });
387
+ it('returns false for non-existent path', () => {
388
+ expect(_isSymlinkSync(path.join(tmpDir, 'nope'))).toBe(false);
389
+ });
390
+ });
391
+ describe('monorepo uninstall with symlink', () => {
392
+ let tmpDir;
393
+ let pluginDir;
394
+ let monoDir;
395
+ beforeEach(() => {
396
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-mono-uninstall-'));
397
+ // We need to use the real PLUGINS_DIR for uninstallPlugin() to work
398
+ pluginDir = path.join(PLUGINS_DIR, '__test-mono-sub__');
399
+ monoDir = path.join(_getMonoreposDir(), '__test-mono__');
400
+ // Set up monorepo structure
401
+ const subDir = path.join(monoDir, 'packages', 'sub');
402
+ fs.mkdirSync(subDir, { recursive: true });
403
+ fs.writeFileSync(path.join(subDir, 'cmd.yaml'), 'site: test');
404
+ // Create symlink in plugins dir
405
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
406
+ fs.symlinkSync(subDir, pluginDir, 'dir');
407
+ // Set up lock file with monorepo entry
408
+ const lock = _readLockFile();
409
+ lock['__test-mono-sub__'] = {
410
+ source: 'https://github.com/user/test.git',
411
+ commitHash: 'abc123',
412
+ installedAt: '2025-01-01T00:00:00.000Z',
413
+ monorepo: { name: '__test-mono__', subPath: 'packages/sub' },
414
+ };
415
+ _writeLockFile(lock);
416
+ });
417
+ afterEach(() => {
418
+ try {
419
+ fs.unlinkSync(pluginDir);
420
+ }
421
+ catch { }
422
+ try {
423
+ fs.rmSync(pluginDir, { recursive: true, force: true });
424
+ }
425
+ catch { }
426
+ try {
427
+ fs.rmSync(monoDir, { recursive: true, force: true });
428
+ }
429
+ catch { }
430
+ // Clean up lock entry
431
+ const lock = _readLockFile();
432
+ delete lock['__test-mono-sub__'];
433
+ _writeLockFile(lock);
434
+ });
435
+ it('removes symlink but keeps monorepo if other sub-plugins reference it', () => {
436
+ // Add another sub-plugin referencing the same monorepo
437
+ const lock = _readLockFile();
438
+ lock['__test-mono-other__'] = {
439
+ source: 'https://github.com/user/test.git',
440
+ commitHash: 'abc123',
441
+ installedAt: '2025-01-01T00:00:00.000Z',
442
+ monorepo: { name: '__test-mono__', subPath: 'packages/other' },
443
+ };
444
+ _writeLockFile(lock);
445
+ uninstallPlugin('__test-mono-sub__');
446
+ // Symlink removed
447
+ expect(fs.existsSync(pluginDir)).toBe(false);
448
+ // Monorepo dir still exists (other sub-plugin references it)
449
+ expect(fs.existsSync(monoDir)).toBe(true);
450
+ // Lock entry removed
451
+ expect(_readLockFile()['__test-mono-sub__']).toBeUndefined();
452
+ // Other lock entry still present
453
+ expect(_readLockFile()['__test-mono-other__']).toBeDefined();
454
+ // Clean up the other entry
455
+ const finalLock = _readLockFile();
456
+ delete finalLock['__test-mono-other__'];
457
+ _writeLockFile(finalLock);
458
+ });
459
+ it('removes symlink AND monorepo dir when last sub-plugin is uninstalled', () => {
460
+ uninstallPlugin('__test-mono-sub__');
461
+ // Symlink removed
462
+ expect(fs.existsSync(pluginDir)).toBe(false);
463
+ // Monorepo dir also removed (no more references)
464
+ expect(fs.existsSync(monoDir)).toBe(false);
465
+ // Lock entry removed
466
+ expect(_readLockFile()['__test-mono-sub__']).toBeUndefined();
467
+ });
468
+ });
469
+ describe('listPlugins with monorepo metadata', () => {
470
+ const testSymlinkTarget = path.join(os.tmpdir(), 'opencli-list-mono-target');
471
+ const testLink = path.join(PLUGINS_DIR, '__test-mono-list__');
472
+ beforeEach(() => {
473
+ // Create a target dir with a command file
474
+ fs.mkdirSync(testSymlinkTarget, { recursive: true });
475
+ fs.writeFileSync(path.join(testSymlinkTarget, 'hello.yaml'), 'site: test\nname: hello\n');
476
+ // Create symlink
477
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
478
+ try {
479
+ fs.unlinkSync(testLink);
480
+ }
481
+ catch { }
482
+ fs.symlinkSync(testSymlinkTarget, testLink, 'dir');
483
+ // Set up lock file with monorepo entry
484
+ const lock = _readLockFile();
485
+ lock['__test-mono-list__'] = {
486
+ source: 'https://github.com/user/test-mono.git',
487
+ commitHash: 'def456def456def456def456def456def456def4',
488
+ installedAt: '2025-01-01T00:00:00.000Z',
489
+ monorepo: { name: 'test-mono', subPath: 'packages/list' },
490
+ };
491
+ _writeLockFile(lock);
492
+ });
493
+ afterEach(() => {
494
+ try {
495
+ fs.unlinkSync(testLink);
496
+ }
497
+ catch { }
498
+ try {
499
+ fs.rmSync(testSymlinkTarget, { recursive: true, force: true });
500
+ }
501
+ catch { }
502
+ const lock = _readLockFile();
503
+ delete lock['__test-mono-list__'];
504
+ _writeLockFile(lock);
505
+ });
506
+ it('lists symlinked plugins with monorepoName', () => {
507
+ const plugins = listPlugins();
508
+ const found = plugins.find(p => p.name === '__test-mono-list__');
509
+ expect(found).toBeDefined();
510
+ expect(found.monorepoName).toBe('test-mono');
511
+ expect(found.commands).toContain('hello');
512
+ expect(found.source).toBe('https://github.com/user/test-mono.git');
513
+ });
514
+ });