@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
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Plugin manifest: reads and validates opencli-plugin.json files.
3
+ *
4
+ * Supports two modes:
5
+ * 1. Single plugin: repo root IS the plugin directory.
6
+ * 2. Monorepo: repo contains multiple plugins declared in `plugins` field.
7
+ */
8
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import { PKG_VERSION } from './version.js';
12
+
13
+ // ── Types ───────────────────────────────────────────────────────────────────
14
+
15
+ export interface SubPluginEntry {
16
+ /** Relative path from repo root to the sub-plugin directory. */
17
+ path: string;
18
+ version?: string;
19
+ description?: string;
20
+ /** Semver range for opencli compatibility (overrides top-level). */
21
+ opencli?: string;
22
+ /** When true, this sub-plugin is skipped during install. */
23
+ disabled?: boolean;
24
+ }
25
+
26
+ export interface PluginManifest {
27
+ /** Plugin name (single-plugin mode). */
28
+ name?: string;
29
+ /** Semantic version of the plugin (single-plugin mode). */
30
+ version?: string;
31
+ /** Semver range for opencli compatibility, e.g. ">=1.0.0". */
32
+ opencli?: string;
33
+ /** Human-readable description. */
34
+ description?: string;
35
+ /** Monorepo sub-plugins. Key = logical plugin name. */
36
+ plugins?: Record<string, SubPluginEntry>;
37
+ }
38
+
39
+ export const MANIFEST_FILENAME = 'opencli-plugin.json';
40
+
41
+ // ── Read / Validate ─────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Read and parse opencli-plugin.json from a directory.
45
+ * Returns null if the file does not exist or is unparseable.
46
+ */
47
+ export function readPluginManifest(dir: string): PluginManifest | null {
48
+ const manifestPath = path.join(dir, MANIFEST_FILENAME);
49
+ try {
50
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
51
+ const parsed = JSON.parse(raw);
52
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
53
+ return null;
54
+ }
55
+ return parsed as PluginManifest;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** Returns true when the manifest declares a monorepo (has `plugins` field). */
62
+ export function isMonorepo(manifest: PluginManifest): boolean {
63
+ return (
64
+ manifest.plugins !== undefined &&
65
+ manifest.plugins !== null &&
66
+ typeof manifest.plugins === 'object' &&
67
+ Object.keys(manifest.plugins).length > 0
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Get the list of enabled sub-plugins from a monorepo manifest.
73
+ * Returns entries sorted by key name.
74
+ */
75
+ export function getEnabledPlugins(
76
+ manifest: PluginManifest,
77
+ ): Array<{ name: string; entry: SubPluginEntry }> {
78
+ if (!manifest.plugins) return [];
79
+ return Object.entries(manifest.plugins)
80
+ .filter(([, entry]) => !entry.disabled)
81
+ .map(([name, entry]) => ({ name, entry }))
82
+ .sort((a, b) => a.name.localeCompare(b.name));
83
+ }
84
+
85
+ // ── Version compatibility ───────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Check if the current opencli version satisfies a semver range string.
89
+ *
90
+ * Supports a simplified subset of semver ranges:
91
+ * ">=1.0.0" – greater than or equal
92
+ * "<=1.5.0" – less than or equal
93
+ * ">1.0.0" – strictly greater
94
+ * "<2.0.0" – strictly less
95
+ * "^1.2.0" – compatible (>=1.2.0 and <2.0.0)
96
+ * "~1.2.0" – patch-level (>=1.2.0 and <1.3.0)
97
+ * "1.2.0" – exact match
98
+ * ">=1.0.0 <2.0.0" – multiple constraints (space-separated, all must match)
99
+ *
100
+ * Returns true if compatible, false if not, and true for empty/undefined
101
+ * ranges (no constraint = always compatible).
102
+ */
103
+ export function checkCompatibility(range: string | undefined): boolean {
104
+ if (!range || range.trim() === '') return true;
105
+ return satisfiesRange(PKG_VERSION, range);
106
+ }
107
+
108
+ /** Parse a version string ("1.2.3") into [major, minor, patch]. */
109
+ export function parseVersion(version: string): [number, number, number] | null {
110
+ const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
111
+ if (!match) return null;
112
+ return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
113
+ }
114
+
115
+ /** Compare two version tuples: -1 if a<b, 0 if equal, 1 if a>b. */
116
+ function compareVersions(
117
+ a: [number, number, number],
118
+ b: [number, number, number],
119
+ ): -1 | 0 | 1 {
120
+ for (let i = 0; i < 3; i++) {
121
+ if (a[i] < b[i]) return -1;
122
+ if (a[i] > b[i]) return 1;
123
+ }
124
+ return 0;
125
+ }
126
+
127
+ /** Check if a version satisfies a single constraint like ">=1.2.0". */
128
+ function satisfiesSingleConstraint(
129
+ version: [number, number, number],
130
+ constraint: string,
131
+ ): boolean {
132
+ const trimmed = constraint.trim();
133
+ if (!trimmed) return true;
134
+
135
+ // ^1.2.0 → >=1.2.0 <2.0.0
136
+ if (trimmed.startsWith('^')) {
137
+ const target = parseVersion(trimmed.slice(1));
138
+ if (!target) return true;
139
+ const upper: [number, number, number] = [target[0] + 1, 0, 0];
140
+ return compareVersions(version, target) >= 0 && compareVersions(version, upper) < 0;
141
+ }
142
+
143
+ // ~1.2.0 → >=1.2.0 <1.3.0
144
+ if (trimmed.startsWith('~')) {
145
+ const target = parseVersion(trimmed.slice(1));
146
+ if (!target) return true;
147
+ const upper: [number, number, number] = [target[0], target[1] + 1, 0];
148
+ return compareVersions(version, target) >= 0 && compareVersions(version, upper) < 0;
149
+ }
150
+
151
+ // >=, <=, >, <, =
152
+ if (trimmed.startsWith('>=')) {
153
+ const target = parseVersion(trimmed.slice(2));
154
+ if (!target) return true;
155
+ return compareVersions(version, target) >= 0;
156
+ }
157
+ if (trimmed.startsWith('<=')) {
158
+ const target = parseVersion(trimmed.slice(2));
159
+ if (!target) return true;
160
+ return compareVersions(version, target) <= 0;
161
+ }
162
+ if (trimmed.startsWith('>')) {
163
+ const target = parseVersion(trimmed.slice(1));
164
+ if (!target) return true;
165
+ return compareVersions(version, target) > 0;
166
+ }
167
+ if (trimmed.startsWith('<')) {
168
+ const target = parseVersion(trimmed.slice(1));
169
+ if (!target) return true;
170
+ return compareVersions(version, target) < 0;
171
+ }
172
+ if (trimmed.startsWith('=')) {
173
+ const target = parseVersion(trimmed.slice(1));
174
+ if (!target) return true;
175
+ return compareVersions(version, target) === 0;
176
+ }
177
+
178
+ // Exact match
179
+ const target = parseVersion(trimmed);
180
+ if (!target) return true;
181
+ return compareVersions(version, target) === 0;
182
+ }
183
+
184
+ /**
185
+ * Check if a version string satisfies a range expression.
186
+ * Space-separated constraints are ANDed together.
187
+ */
188
+ export function satisfiesRange(versionStr: string, range: string): boolean {
189
+ const version = parseVersion(versionStr);
190
+ if (!version) return true; // Can't parse our own version → assume ok
191
+
192
+ // Split on whitespace for multi-constraint ranges (e.g. ">=1.0.0 <2.0.0")
193
+ const constraints = range.trim().split(/\s+/);
194
+ return constraints.every((c) => satisfiesSingleConstraint(version, c));
195
+ }
196
+
197
+ // ── Exports for testing ─────────────────────────────────────────────────────
198
+
199
+ export {
200
+ readPluginManifest as _readPluginManifest,
201
+ isMonorepo as _isMonorepo,
202
+ getEnabledPlugins as _getEnabledPlugins,
203
+ checkCompatibility as _checkCompatibility,
204
+ parseVersion as _parseVersion,
205
+ satisfiesRange as _satisfiesRange,
206
+ };
@@ -10,9 +10,16 @@ import { PLUGINS_DIR } from './discovery.js';
10
10
  import type { LockEntry } from './plugin.js';
11
11
  import * as pluginModule from './plugin.js';
12
12
 
13
+ const { mockExecFileSync, mockExecSync } = vi.hoisted(() => ({
14
+ mockExecFileSync: vi.fn(),
15
+ mockExecSync: vi.fn(),
16
+ }));
17
+
13
18
  const {
14
19
  LOCK_FILE,
15
20
  _getCommitHash,
21
+ _installDependencies,
22
+ _postInstallMonorepoLifecycle,
16
23
  listPlugins,
17
24
  _readLockFile,
18
25
  _resolveEsbuildBin,
@@ -22,6 +29,8 @@ const {
22
29
  _updateAllPlugins,
23
30
  _validatePluginStructure,
24
31
  _writeLockFile,
32
+ _isSymlinkSync,
33
+ _getMonoreposDir,
25
34
  } = pluginModule;
26
35
 
27
36
  describe('parseSource', () => {
@@ -283,7 +292,7 @@ describe('updatePlugin', () => {
283
292
 
284
293
  vi.mock('node:child_process', () => {
285
294
  return {
286
- execFileSync: vi.fn((_cmd, args, opts) => {
295
+ execFileSync: mockExecFileSync.mockImplementation((_cmd, args, opts) => {
287
296
  if (Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
288
297
  if (opts?.cwd === os.tmpdir()) {
289
298
  throw new Error('not a git repository');
@@ -295,10 +304,63 @@ vi.mock('node:child_process', () => {
295
304
  }
296
305
  return '';
297
306
  }),
298
- execSync: vi.fn(() => ''),
307
+ execSync: mockExecSync.mockImplementation(() => ''),
299
308
  };
300
309
  });
301
310
 
311
+ describe('installDependencies', () => {
312
+ beforeEach(() => {
313
+ mockExecFileSync.mockClear();
314
+ mockExecSync.mockClear();
315
+ });
316
+
317
+ it('throws when npm install fails', () => {
318
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-plugin-b-'));
319
+ const failingDir = path.join(tmpDir, 'plugin-b');
320
+ fs.mkdirSync(failingDir, { recursive: true });
321
+ fs.writeFileSync(path.join(failingDir, 'package.json'), JSON.stringify({ name: 'plugin-b' }));
322
+
323
+ expect(() => _installDependencies(failingDir)).toThrow('npm install failed');
324
+
325
+ fs.rmSync(tmpDir, { recursive: true, force: true });
326
+ });
327
+ });
328
+
329
+ describe('postInstallMonorepoLifecycle', () => {
330
+ let repoDir: string;
331
+ let subDir: string;
332
+
333
+ beforeEach(() => {
334
+ mockExecFileSync.mockClear();
335
+ mockExecSync.mockClear();
336
+ repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-monorepo-'));
337
+ subDir = path.join(repoDir, 'packages', 'alpha');
338
+ fs.mkdirSync(subDir, { recursive: true });
339
+ fs.writeFileSync(path.join(repoDir, 'package.json'), JSON.stringify({
340
+ name: 'opencli-plugins',
341
+ private: true,
342
+ workspaces: ['packages/*'],
343
+ }));
344
+ fs.writeFileSync(path.join(subDir, 'hello.yaml'), 'site: test\nname: hello\n');
345
+ });
346
+
347
+ afterEach(() => {
348
+ fs.rmSync(repoDir, { recursive: true, force: true });
349
+ });
350
+
351
+ it('installs dependencies once at the monorepo root, not in each sub-plugin', () => {
352
+ _postInstallMonorepoLifecycle(repoDir, [subDir]);
353
+
354
+ const npmCalls = mockExecFileSync.mock.calls.filter(
355
+ ([cmd, args]) => cmd === 'npm' && Array.isArray(args) && args[0] === 'install',
356
+ );
357
+
358
+ expect(npmCalls).toHaveLength(1);
359
+ expect(npmCalls[0][2]).toMatchObject({ cwd: repoDir });
360
+ expect(npmCalls.some(([, , opts]) => opts?.cwd === subDir)).toBe(false);
361
+ });
362
+ });
363
+
302
364
  describe('updateAllPlugins', () => {
303
365
  const testDirA = path.join(PLUGINS_DIR, 'plugin-a');
304
366
  const testDirB = path.join(PLUGINS_DIR, 'plugin-b');
@@ -338,3 +400,185 @@ describe('updateAllPlugins', () => {
338
400
  expect(resC!.success).toBe(true);
339
401
  });
340
402
  });
403
+
404
+ // ── Monorepo-specific tests ─────────────────────────────────────────────────
405
+
406
+ describe('parseSource with monorepo subplugin', () => {
407
+ it('parses github:user/repo/subplugin format', () => {
408
+ const result = _parseSource('github:ByteYue/opencli-plugins/polymarket');
409
+ expect(result).toEqual({
410
+ cloneUrl: 'https://github.com/ByteYue/opencli-plugins.git',
411
+ name: 'opencli-plugins',
412
+ subPlugin: 'polymarket',
413
+ });
414
+ });
415
+
416
+ it('strips opencli-plugin- prefix from repo name in subplugin format', () => {
417
+ const result = _parseSource('github:user/opencli-plugin-collection/defi');
418
+ expect(result!.name).toBe('collection');
419
+ expect(result!.subPlugin).toBe('defi');
420
+ });
421
+
422
+ it('still parses github:user/repo without subplugin', () => {
423
+ const result = _parseSource('github:user/my-repo');
424
+ expect(result).toEqual({
425
+ cloneUrl: 'https://github.com/user/my-repo.git',
426
+ name: 'my-repo',
427
+ });
428
+ expect(result!.subPlugin).toBeUndefined();
429
+ });
430
+ });
431
+
432
+ describe('isSymlinkSync', () => {
433
+ let tmpDir: string;
434
+
435
+ beforeEach(() => {
436
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-symlink-test-'));
437
+ });
438
+
439
+ afterEach(() => {
440
+ fs.rmSync(tmpDir, { recursive: true, force: true });
441
+ });
442
+
443
+ it('returns false for a regular directory', () => {
444
+ const dir = path.join(tmpDir, 'regular');
445
+ fs.mkdirSync(dir);
446
+ expect(_isSymlinkSync(dir)).toBe(false);
447
+ });
448
+
449
+ it('returns true for a symlink', () => {
450
+ const target = path.join(tmpDir, 'target');
451
+ const link = path.join(tmpDir, 'link');
452
+ fs.mkdirSync(target);
453
+ fs.symlinkSync(target, link, 'dir');
454
+ expect(_isSymlinkSync(link)).toBe(true);
455
+ });
456
+
457
+ it('returns false for non-existent path', () => {
458
+ expect(_isSymlinkSync(path.join(tmpDir, 'nope'))).toBe(false);
459
+ });
460
+ });
461
+
462
+ describe('monorepo uninstall with symlink', () => {
463
+ let tmpDir: string;
464
+ let pluginDir: string;
465
+ let monoDir: string;
466
+
467
+ beforeEach(() => {
468
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-mono-uninstall-'));
469
+ // We need to use the real PLUGINS_DIR for uninstallPlugin() to work
470
+ pluginDir = path.join(PLUGINS_DIR, '__test-mono-sub__');
471
+ monoDir = path.join(_getMonoreposDir(), '__test-mono__');
472
+
473
+ // Set up monorepo structure
474
+ const subDir = path.join(monoDir, 'packages', 'sub');
475
+ fs.mkdirSync(subDir, { recursive: true });
476
+ fs.writeFileSync(path.join(subDir, 'cmd.yaml'), 'site: test');
477
+
478
+ // Create symlink in plugins dir
479
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
480
+ fs.symlinkSync(subDir, pluginDir, 'dir');
481
+
482
+ // Set up lock file with monorepo entry
483
+ const lock = _readLockFile();
484
+ lock['__test-mono-sub__'] = {
485
+ source: 'https://github.com/user/test.git',
486
+ commitHash: 'abc123',
487
+ installedAt: '2025-01-01T00:00:00.000Z',
488
+ monorepo: { name: '__test-mono__', subPath: 'packages/sub' },
489
+ };
490
+ _writeLockFile(lock);
491
+ });
492
+
493
+ afterEach(() => {
494
+ try { fs.unlinkSync(pluginDir); } catch {}
495
+ try { fs.rmSync(pluginDir, { recursive: true, force: true }); } catch {}
496
+ try { fs.rmSync(monoDir, { recursive: true, force: true }); } catch {}
497
+ // Clean up lock entry
498
+ const lock = _readLockFile();
499
+ delete lock['__test-mono-sub__'];
500
+ _writeLockFile(lock);
501
+ });
502
+
503
+ it('removes symlink but keeps monorepo if other sub-plugins reference it', () => {
504
+ // Add another sub-plugin referencing the same monorepo
505
+ const lock = _readLockFile();
506
+ lock['__test-mono-other__'] = {
507
+ source: 'https://github.com/user/test.git',
508
+ commitHash: 'abc123',
509
+ installedAt: '2025-01-01T00:00:00.000Z',
510
+ monorepo: { name: '__test-mono__', subPath: 'packages/other' },
511
+ };
512
+ _writeLockFile(lock);
513
+
514
+ uninstallPlugin('__test-mono-sub__');
515
+
516
+ // Symlink removed
517
+ expect(fs.existsSync(pluginDir)).toBe(false);
518
+ // Monorepo dir still exists (other sub-plugin references it)
519
+ expect(fs.existsSync(monoDir)).toBe(true);
520
+ // Lock entry removed
521
+ expect(_readLockFile()['__test-mono-sub__']).toBeUndefined();
522
+ // Other lock entry still present
523
+ expect(_readLockFile()['__test-mono-other__']).toBeDefined();
524
+
525
+ // Clean up the other entry
526
+ const finalLock = _readLockFile();
527
+ delete finalLock['__test-mono-other__'];
528
+ _writeLockFile(finalLock);
529
+ });
530
+
531
+ it('removes symlink AND monorepo dir when last sub-plugin is uninstalled', () => {
532
+ uninstallPlugin('__test-mono-sub__');
533
+
534
+ // Symlink removed
535
+ expect(fs.existsSync(pluginDir)).toBe(false);
536
+ // Monorepo dir also removed (no more references)
537
+ expect(fs.existsSync(monoDir)).toBe(false);
538
+ // Lock entry removed
539
+ expect(_readLockFile()['__test-mono-sub__']).toBeUndefined();
540
+ });
541
+ });
542
+
543
+ describe('listPlugins with monorepo metadata', () => {
544
+ const testSymlinkTarget = path.join(os.tmpdir(), 'opencli-list-mono-target');
545
+ const testLink = path.join(PLUGINS_DIR, '__test-mono-list__');
546
+
547
+ beforeEach(() => {
548
+ // Create a target dir with a command file
549
+ fs.mkdirSync(testSymlinkTarget, { recursive: true });
550
+ fs.writeFileSync(path.join(testSymlinkTarget, 'hello.yaml'), 'site: test\nname: hello\n');
551
+
552
+ // Create symlink
553
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
554
+ try { fs.unlinkSync(testLink); } catch {}
555
+ fs.symlinkSync(testSymlinkTarget, testLink, 'dir');
556
+
557
+ // Set up lock file with monorepo entry
558
+ const lock = _readLockFile();
559
+ lock['__test-mono-list__'] = {
560
+ source: 'https://github.com/user/test-mono.git',
561
+ commitHash: 'def456def456def456def456def456def456def4',
562
+ installedAt: '2025-01-01T00:00:00.000Z',
563
+ monorepo: { name: 'test-mono', subPath: 'packages/list' },
564
+ };
565
+ _writeLockFile(lock);
566
+ });
567
+
568
+ afterEach(() => {
569
+ try { fs.unlinkSync(testLink); } catch {}
570
+ try { fs.rmSync(testSymlinkTarget, { recursive: true, force: true }); } catch {}
571
+ const lock = _readLockFile();
572
+ delete lock['__test-mono-list__'];
573
+ _writeLockFile(lock);
574
+ });
575
+
576
+ it('lists symlinked plugins with monorepoName', () => {
577
+ const plugins = listPlugins();
578
+ const found = plugins.find(p => p.name === '__test-mono-list__');
579
+ expect(found).toBeDefined();
580
+ expect(found!.monorepoName).toBe('test-mono');
581
+ expect(found!.commands).toContain('hello');
582
+ expect(found!.source).toBe('https://github.com/user/test-mono.git');
583
+ });
584
+ });