@jackwener/opencli 1.3.3 → 1.4.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 (496) hide show
  1. package/.github/pull_request_template.md +3 -1
  2. package/.github/workflows/build-extension.yml +7 -1
  3. package/.github/workflows/ci.yml +29 -3
  4. package/.github/workflows/docs.yml +1 -1
  5. package/.github/workflows/e2e-headed.yml +20 -0
  6. package/.github/workflows/release.yml +1 -1
  7. package/.github/workflows/security.yml +0 -3
  8. package/CHANGELOG.md +55 -0
  9. package/CONTRIBUTING.md +6 -3
  10. package/README.md +30 -3
  11. package/README.zh-CN.md +30 -3
  12. package/SKILL.md +7 -1
  13. package/TESTING.md +1 -0
  14. package/chatwise-opencli.ps1 +82 -0
  15. package/dist/analysis.d.ts +38 -0
  16. package/dist/analysis.js +166 -0
  17. package/dist/browser/cdp.d.ts +0 -4
  18. package/dist/browser/cdp.js +53 -41
  19. package/dist/browser/cdp.test.d.ts +1 -0
  20. package/dist/browser/cdp.test.js +52 -0
  21. package/dist/browser/dom-snapshot.d.ts +2 -2
  22. package/dist/browser/dom-snapshot.js +54 -1
  23. package/dist/browser/dom-snapshot.test.js +36 -0
  24. package/dist/browser/index.d.ts +2 -2
  25. package/dist/browser/index.js +1 -1
  26. package/dist/browser/mcp.d.ts +0 -2
  27. package/dist/browser/mcp.js +2 -3
  28. package/dist/browser/page.d.ts +4 -3
  29. package/dist/browser/page.js +34 -37
  30. package/dist/browser/stealth.d.ts +0 -2
  31. package/dist/browser/stealth.js +24 -9
  32. package/dist/browser.test.js +2 -2
  33. package/dist/build-manifest.js +15 -9
  34. package/dist/build-manifest.test.js +12 -0
  35. package/dist/cascade.js +4 -2
  36. package/dist/cli-manifest.json +639 -258
  37. package/dist/cli.js +57 -29
  38. package/dist/clis/_shared/desktop-commands.d.ts +22 -0
  39. package/dist/clis/_shared/desktop-commands.js +108 -0
  40. package/dist/clis/antigravity/serve.js +5 -2
  41. package/dist/clis/arxiv/search.js +1 -1
  42. package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
  43. package/dist/clis/bilibili/dynamic.test.js +68 -0
  44. package/dist/clis/bilibili/favorite.js +4 -2
  45. package/dist/clis/bilibili/following.js +3 -2
  46. package/dist/clis/bilibili/subtitle.js +8 -7
  47. package/dist/clis/bilibili/utils.js +2 -2
  48. package/dist/clis/boss/batchgreet.js +1 -1
  49. package/dist/clis/boss/chatlist.js +1 -1
  50. package/dist/clis/boss/chatmsg.js +1 -1
  51. package/dist/clis/boss/detail.js +1 -1
  52. package/dist/clis/boss/exchange.js +1 -1
  53. package/dist/clis/boss/greet.js +1 -1
  54. package/dist/clis/boss/invite.js +1 -1
  55. package/dist/clis/boss/joblist.js +1 -1
  56. package/dist/clis/boss/mark.js +4 -3
  57. package/dist/clis/boss/recommend.js +1 -1
  58. package/dist/clis/boss/resume.js +1 -1
  59. package/dist/clis/boss/search.js +1 -1
  60. package/dist/clis/boss/send.js +5 -4
  61. package/dist/clis/boss/stats.js +1 -1
  62. package/dist/clis/chatgpt/ask.js +4 -0
  63. package/dist/clis/chatgpt/new.js +5 -1
  64. package/dist/clis/chatgpt/read.js +5 -1
  65. package/dist/clis/chatgpt/send.js +2 -1
  66. package/dist/clis/chatgpt/status.js +5 -1
  67. package/dist/clis/chatwise/ask.js +8 -2
  68. package/dist/clis/chatwise/export.js +2 -0
  69. package/dist/clis/chatwise/history.js +2 -0
  70. package/dist/clis/chatwise/model.js +8 -3
  71. package/dist/clis/chatwise/new.js +3 -18
  72. package/dist/clis/chatwise/read.js +2 -0
  73. package/dist/clis/chatwise/screenshot.js +3 -27
  74. package/dist/clis/chatwise/send.js +8 -2
  75. package/dist/clis/chatwise/shared.d.ts +2 -0
  76. package/dist/clis/chatwise/shared.js +6 -0
  77. package/dist/clis/chatwise/status.js +3 -22
  78. package/dist/clis/codex/ask.js +6 -2
  79. package/dist/clis/codex/dump.js +2 -25
  80. package/dist/clis/codex/new.js +2 -25
  81. package/dist/clis/codex/screenshot.js +2 -27
  82. package/dist/clis/codex/send.js +6 -4
  83. package/dist/clis/codex/status.js +2 -22
  84. package/dist/clis/cursor/ask.js +2 -1
  85. package/dist/clis/cursor/composer.js +2 -1
  86. package/dist/clis/cursor/dump.js +2 -25
  87. package/dist/clis/cursor/new.js +2 -18
  88. package/dist/clis/cursor/read.js +2 -1
  89. package/dist/clis/cursor/screenshot.js +1 -30
  90. package/dist/clis/cursor/send.js +2 -1
  91. package/dist/clis/cursor/status.js +2 -21
  92. package/dist/clis/dictionary/examples.yaml +25 -0
  93. package/dist/clis/dictionary/search.yaml +27 -0
  94. package/dist/clis/dictionary/synonyms.yaml +25 -0
  95. package/dist/clis/douban/book-hot.js +1 -1
  96. package/dist/clis/douban/movie-hot.js +1 -1
  97. package/dist/clis/douban/search.js +1 -1
  98. package/dist/clis/douban/utils.d.ts +4 -1
  99. package/dist/clis/douban/utils.js +156 -1
  100. package/dist/clis/doubao/ask.js +1 -1
  101. package/dist/clis/doubao/new.js +1 -1
  102. package/dist/clis/doubao/read.js +1 -1
  103. package/dist/clis/doubao/send.js +1 -1
  104. package/dist/clis/doubao/status.js +1 -1
  105. package/dist/clis/doubao-app/ask.js +1 -1
  106. package/dist/clis/doubao-app/new.js +1 -1
  107. package/dist/clis/doubao-app/read.js +1 -1
  108. package/dist/clis/doubao-app/send.js +1 -1
  109. package/dist/clis/grok/ask.d.ts +4 -0
  110. package/dist/clis/grok/ask.js +28 -10
  111. package/dist/clis/grok/ask.test.js +18 -0
  112. package/dist/clis/jd/item.d.ts +1 -0
  113. package/dist/clis/jd/item.js +96 -0
  114. package/dist/clis/jd/item.test.d.ts +1 -0
  115. package/dist/clis/jd/item.test.js +28 -0
  116. package/dist/clis/jike/feed.js +1 -1
  117. package/dist/clis/jike/search.js +1 -1
  118. package/dist/clis/linkedin/search.js +5 -4
  119. package/dist/clis/linkedin/timeline.d.ts +21 -0
  120. package/dist/clis/linkedin/timeline.js +503 -0
  121. package/dist/clis/linkedin/timeline.test.d.ts +1 -0
  122. package/dist/clis/linkedin/timeline.test.js +81 -0
  123. package/dist/clis/medium/feed.js +1 -1
  124. package/dist/clis/medium/search.js +1 -1
  125. package/dist/clis/medium/user.js +1 -1
  126. package/dist/clis/medium/{shared.js → utils.js} +2 -1
  127. package/dist/clis/pixiv/detail.yaml +49 -0
  128. package/dist/clis/pixiv/download.d.ts +7 -0
  129. package/dist/clis/pixiv/download.js +78 -0
  130. package/dist/clis/pixiv/download.test.d.ts +1 -0
  131. package/dist/clis/pixiv/download.test.js +87 -0
  132. package/dist/clis/pixiv/illusts.d.ts +8 -0
  133. package/dist/clis/pixiv/illusts.js +65 -0
  134. package/dist/clis/pixiv/illusts.test.d.ts +1 -0
  135. package/dist/clis/pixiv/illusts.test.js +99 -0
  136. package/dist/clis/pixiv/ranking.yaml +53 -0
  137. package/dist/clis/pixiv/search.d.ts +6 -0
  138. package/dist/clis/pixiv/search.js +43 -0
  139. package/dist/clis/pixiv/search.test.d.ts +1 -0
  140. package/dist/clis/pixiv/search.test.js +83 -0
  141. package/dist/clis/pixiv/test-utils.d.ts +12 -0
  142. package/dist/clis/pixiv/test-utils.js +23 -0
  143. package/dist/clis/pixiv/user.yaml +46 -0
  144. package/dist/clis/pixiv/utils.d.ts +27 -0
  145. package/dist/clis/pixiv/utils.js +49 -0
  146. package/dist/clis/reddit/comment.js +2 -1
  147. package/dist/clis/reddit/read.js +4 -3
  148. package/dist/clis/reddit/read.test.d.ts +1 -0
  149. package/dist/clis/reddit/read.test.js +28 -0
  150. package/dist/clis/reddit/save.js +2 -1
  151. package/dist/clis/reddit/saved.js +7 -3
  152. package/dist/clis/reddit/subscribe.js +2 -1
  153. package/dist/clis/reddit/upvote.js +2 -1
  154. package/dist/clis/reddit/upvoted.js +7 -3
  155. package/dist/clis/sinablog/article.js +1 -1
  156. package/dist/clis/sinablog/hot.js +1 -1
  157. package/dist/clis/sinablog/user.js +1 -1
  158. package/dist/clis/substack/feed.js +1 -1
  159. package/dist/clis/substack/publication.js +1 -1
  160. package/dist/clis/substack/search.js +3 -2
  161. package/dist/clis/substack/{shared.js → utils.js} +3 -2
  162. package/dist/clis/tiktok/search.yaml +2 -1
  163. package/dist/clis/twitter/accept.js +2 -1
  164. package/dist/clis/twitter/article.js +4 -1
  165. package/dist/clis/twitter/block.js +2 -1
  166. package/dist/clis/twitter/bookmark.js +2 -1
  167. package/dist/clis/twitter/bookmarks.js +3 -2
  168. package/dist/clis/twitter/delete.js +2 -1
  169. package/dist/clis/twitter/follow.js +2 -1
  170. package/dist/clis/twitter/followers.js +3 -2
  171. package/dist/clis/twitter/following.js +3 -2
  172. package/dist/clis/twitter/hide-reply.js +2 -1
  173. package/dist/clis/twitter/like.js +2 -1
  174. package/dist/clis/twitter/notifications.js +2 -1
  175. package/dist/clis/twitter/post.js +2 -1
  176. package/dist/clis/twitter/profile.js +5 -2
  177. package/dist/clis/twitter/reply-dm.js +2 -1
  178. package/dist/clis/twitter/reply.js +2 -1
  179. package/dist/clis/twitter/search.js +30 -13
  180. package/dist/clis/twitter/search.test.d.ts +1 -0
  181. package/dist/clis/twitter/search.test.js +104 -0
  182. package/dist/clis/twitter/thread.js +2 -2
  183. package/dist/clis/twitter/timeline.js +3 -2
  184. package/dist/clis/twitter/trending.js +3 -2
  185. package/dist/clis/twitter/unblock.js +2 -1
  186. package/dist/clis/twitter/unbookmark.js +2 -1
  187. package/dist/clis/twitter/unfollow.js +2 -1
  188. package/dist/clis/v2ex/daily.js +3 -2
  189. package/dist/clis/v2ex/me.js +3 -2
  190. package/dist/clis/v2ex/notifications.js +4 -4
  191. package/dist/clis/web/read.d.ts +16 -0
  192. package/dist/clis/web/read.js +202 -0
  193. package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
  194. package/dist/clis/xueqiu/danjuan-utils.js +126 -0
  195. package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
  196. package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
  197. package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
  198. package/dist/clis/xueqiu/fund-holdings.js +28 -0
  199. package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
  200. package/dist/clis/xueqiu/fund-snapshot.js +25 -0
  201. package/dist/clis/youtube/transcript.js +5 -4
  202. package/dist/clis/youtube/video.js +3 -2
  203. package/dist/daemon.js +7 -3
  204. package/dist/discovery.js +11 -10
  205. package/dist/doctor.js +2 -1
  206. package/dist/download/index.d.ts +4 -12
  207. package/dist/download/index.js +33 -12
  208. package/dist/download/index.test.js +79 -2
  209. package/dist/download/media-download.js +4 -2
  210. package/dist/engine.test.js +76 -4
  211. package/dist/execution.d.ts +1 -9
  212. package/dist/execution.js +56 -46
  213. package/dist/explore.js +12 -111
  214. package/dist/external-clis.yaml +0 -8
  215. package/dist/external.js +7 -5
  216. package/dist/external.test.js +4 -0
  217. package/dist/generate.d.ts +0 -9
  218. package/dist/generate.js +4 -20
  219. package/dist/hooks.d.ts +46 -0
  220. package/dist/hooks.js +56 -0
  221. package/dist/hooks.test.d.ts +4 -0
  222. package/dist/hooks.test.js +92 -0
  223. package/dist/interceptor.js +70 -23
  224. package/dist/main.js +2 -0
  225. package/dist/output.js +12 -6
  226. package/dist/pipeline/executor.js +1 -1
  227. package/dist/pipeline/steps/browser.js +1 -3
  228. package/dist/pipeline/steps/download.js +42 -26
  229. package/dist/pipeline/steps/download.test.d.ts +1 -0
  230. package/dist/pipeline/steps/download.test.js +101 -0
  231. package/dist/pipeline/steps/fetch.js +40 -22
  232. package/dist/pipeline/steps/fetch.test.d.ts +1 -0
  233. package/dist/pipeline/steps/fetch.test.js +123 -0
  234. package/dist/pipeline/steps/transform.js +2 -6
  235. package/dist/pipeline/template.js +66 -52
  236. package/dist/pipeline/template.test.js +28 -0
  237. package/dist/pipeline/transform.test.js +18 -0
  238. package/dist/plugin.d.ts +40 -1
  239. package/dist/plugin.js +214 -17
  240. package/dist/plugin.test.d.ts +1 -1
  241. package/dist/plugin.test.js +219 -3
  242. package/dist/record.js +6 -98
  243. package/dist/registry-api.d.ts +2 -0
  244. package/dist/registry-api.js +1 -0
  245. package/dist/registry.d.ts +5 -2
  246. package/dist/registry.js +1 -2
  247. package/dist/runtime.d.ts +0 -1
  248. package/dist/runtime.js +14 -4
  249. package/dist/snapshotFormatter.d.ts +7 -14
  250. package/dist/snapshotFormatter.js +38 -78
  251. package/dist/utils.d.ts +9 -0
  252. package/dist/utils.js +29 -0
  253. package/dist/validate.js +3 -5
  254. package/dist/yaml-schema.d.ts +26 -0
  255. package/dist/yaml-schema.js +5 -0
  256. package/docs/.vitepress/config.mts +3 -0
  257. package/docs/adapters/browser/dictionary.md +27 -0
  258. package/docs/adapters/browser/jd.md +27 -0
  259. package/docs/adapters/browser/linkedin.md +6 -0
  260. package/docs/adapters/browser/pixiv.md +92 -0
  261. package/docs/adapters/browser/web.md +30 -0
  262. package/docs/adapters/browser/xueqiu.md +27 -9
  263. package/docs/adapters/index.md +3 -1
  264. package/docs/comparison.md +125 -0
  265. package/docs/developer/contributing.md +21 -2
  266. package/docs/developer/testing.md +14 -8
  267. package/docs/developer/ts-adapter.md +18 -0
  268. package/docs/developer/yaml-adapter.md +16 -0
  269. package/docs/guide/plugins.md +10 -0
  270. package/docs/zh/guide/plugins.md +10 -0
  271. package/extension/dist/background.js +519 -444
  272. package/extension/manifest.json +1 -1
  273. package/extension/package.json +1 -1
  274. package/extension/src/background.test.ts +46 -1
  275. package/extension/src/background.ts +108 -33
  276. package/extension/src/cdp.ts +9 -9
  277. package/package.json +3 -2
  278. package/scripts/check-doc-coverage.sh +2 -0
  279. package/src/analysis.ts +170 -0
  280. package/src/browser/cdp.test.ts +66 -0
  281. package/src/browser/cdp.ts +59 -44
  282. package/src/browser/dom-snapshot.test.ts +42 -0
  283. package/src/browser/dom-snapshot.ts +56 -3
  284. package/src/browser/index.ts +2 -2
  285. package/src/browser/mcp.ts +2 -4
  286. package/src/browser/page.ts +34 -37
  287. package/src/browser/stealth.ts +24 -10
  288. package/src/browser.test.ts +2 -2
  289. package/src/build-manifest.test.ts +14 -0
  290. package/src/build-manifest.ts +13 -31
  291. package/src/cascade.ts +5 -3
  292. package/src/cli.ts +66 -34
  293. package/src/clis/_shared/desktop-commands.ts +121 -0
  294. package/src/clis/antigravity/serve.ts +6 -3
  295. package/src/clis/arxiv/search.ts +1 -1
  296. package/src/clis/bilibili/dynamic.test.ts +79 -0
  297. package/src/clis/bilibili/favorite.ts +5 -2
  298. package/src/clis/bilibili/following.ts +3 -2
  299. package/src/clis/bilibili/subtitle.ts +8 -7
  300. package/src/clis/bilibili/utils.ts +2 -2
  301. package/src/clis/boss/batchgreet.ts +1 -1
  302. package/src/clis/boss/chatlist.ts +1 -1
  303. package/src/clis/boss/chatmsg.ts +1 -1
  304. package/src/clis/boss/detail.ts +1 -1
  305. package/src/clis/boss/exchange.ts +1 -1
  306. package/src/clis/boss/greet.ts +1 -1
  307. package/src/clis/boss/invite.ts +1 -1
  308. package/src/clis/boss/joblist.ts +1 -1
  309. package/src/clis/boss/mark.ts +4 -3
  310. package/src/clis/boss/recommend.ts +1 -1
  311. package/src/clis/boss/resume.ts +1 -1
  312. package/src/clis/boss/search.ts +1 -1
  313. package/src/clis/boss/send.ts +5 -4
  314. package/src/clis/boss/stats.ts +1 -1
  315. package/src/clis/chatgpt/ask.ts +5 -0
  316. package/src/clis/chatgpt/new.ts +7 -2
  317. package/src/clis/chatgpt/read.ts +7 -2
  318. package/src/clis/chatgpt/send.ts +3 -2
  319. package/src/clis/chatgpt/status.ts +6 -1
  320. package/src/clis/chatwise/ask.ts +7 -2
  321. package/src/clis/chatwise/export.ts +2 -0
  322. package/src/clis/chatwise/history.ts +2 -0
  323. package/src/clis/chatwise/model.ts +7 -3
  324. package/src/clis/chatwise/new.ts +3 -20
  325. package/src/clis/chatwise/read.ts +2 -0
  326. package/src/clis/chatwise/screenshot.ts +3 -32
  327. package/src/clis/chatwise/send.ts +7 -2
  328. package/src/clis/chatwise/shared.ts +8 -0
  329. package/src/clis/chatwise/status.ts +3 -24
  330. package/src/clis/codex/ask.ts +5 -2
  331. package/src/clis/codex/dump.ts +2 -27
  332. package/src/clis/codex/new.ts +2 -28
  333. package/src/clis/codex/screenshot.ts +2 -32
  334. package/src/clis/codex/send.ts +5 -4
  335. package/src/clis/codex/status.ts +2 -24
  336. package/src/clis/cursor/ask.ts +2 -1
  337. package/src/clis/cursor/composer.ts +2 -1
  338. package/src/clis/cursor/dump.ts +2 -27
  339. package/src/clis/cursor/new.ts +2 -20
  340. package/src/clis/cursor/read.ts +2 -1
  341. package/src/clis/cursor/screenshot.ts +1 -36
  342. package/src/clis/cursor/send.ts +2 -1
  343. package/src/clis/cursor/status.ts +2 -22
  344. package/src/clis/dictionary/examples.yaml +25 -0
  345. package/src/clis/dictionary/search.yaml +27 -0
  346. package/src/clis/dictionary/synonyms.yaml +25 -0
  347. package/src/clis/douban/book-hot.ts +1 -1
  348. package/src/clis/douban/movie-hot.ts +1 -1
  349. package/src/clis/douban/search.ts +1 -1
  350. package/src/clis/douban/utils.ts +165 -1
  351. package/src/clis/doubao/ask.ts +1 -1
  352. package/src/clis/doubao/new.ts +1 -1
  353. package/src/clis/doubao/read.ts +1 -1
  354. package/src/clis/doubao/send.ts +1 -1
  355. package/src/clis/doubao/status.ts +1 -1
  356. package/src/clis/doubao-app/ask.ts +1 -1
  357. package/src/clis/doubao-app/new.ts +1 -1
  358. package/src/clis/doubao-app/read.ts +1 -1
  359. package/src/clis/doubao-app/send.ts +1 -1
  360. package/src/clis/grok/ask.test.ts +25 -0
  361. package/src/clis/grok/ask.ts +25 -12
  362. package/src/clis/jd/item.test.ts +35 -0
  363. package/src/clis/jd/item.ts +101 -0
  364. package/src/clis/jike/feed.ts +1 -1
  365. package/src/clis/jike/search.ts +1 -1
  366. package/src/clis/linkedin/search.ts +5 -4
  367. package/src/clis/linkedin/timeline.test.ts +99 -0
  368. package/src/clis/linkedin/timeline.ts +532 -0
  369. package/src/clis/medium/feed.ts +1 -1
  370. package/src/clis/medium/search.ts +1 -1
  371. package/src/clis/medium/user.ts +1 -1
  372. package/src/clis/medium/{shared.ts → utils.ts} +2 -1
  373. package/src/clis/pixiv/detail.yaml +49 -0
  374. package/src/clis/pixiv/download.test.ts +114 -0
  375. package/src/clis/pixiv/download.ts +91 -0
  376. package/src/clis/pixiv/illusts.test.ts +115 -0
  377. package/src/clis/pixiv/illusts.ts +78 -0
  378. package/src/clis/pixiv/ranking.yaml +53 -0
  379. package/src/clis/pixiv/search.test.ts +97 -0
  380. package/src/clis/pixiv/search.ts +53 -0
  381. package/src/clis/pixiv/test-utils.ts +29 -0
  382. package/src/clis/pixiv/user.yaml +46 -0
  383. package/src/clis/pixiv/utils.ts +62 -0
  384. package/src/clis/reddit/comment.ts +2 -1
  385. package/src/clis/reddit/read.test.ts +34 -0
  386. package/src/clis/reddit/read.ts +4 -3
  387. package/src/clis/reddit/save.ts +2 -1
  388. package/src/clis/reddit/saved.ts +6 -2
  389. package/src/clis/reddit/subscribe.ts +2 -1
  390. package/src/clis/reddit/upvote.ts +2 -1
  391. package/src/clis/reddit/upvoted.ts +6 -2
  392. package/src/clis/sinablog/article.ts +1 -1
  393. package/src/clis/sinablog/hot.ts +1 -1
  394. package/src/clis/sinablog/user.ts +1 -1
  395. package/src/clis/substack/feed.ts +1 -1
  396. package/src/clis/substack/publication.ts +1 -1
  397. package/src/clis/substack/search.ts +3 -2
  398. package/src/clis/substack/{shared.ts → utils.ts} +3 -2
  399. package/src/clis/tiktok/search.yaml +2 -1
  400. package/src/clis/twitter/accept.ts +2 -1
  401. package/src/clis/twitter/article.ts +3 -1
  402. package/src/clis/twitter/block.ts +2 -1
  403. package/src/clis/twitter/bookmark.ts +2 -1
  404. package/src/clis/twitter/bookmarks.ts +3 -2
  405. package/src/clis/twitter/delete.ts +2 -1
  406. package/src/clis/twitter/follow.ts +2 -1
  407. package/src/clis/twitter/followers.ts +3 -2
  408. package/src/clis/twitter/following.ts +3 -2
  409. package/src/clis/twitter/hide-reply.ts +2 -1
  410. package/src/clis/twitter/like.ts +2 -1
  411. package/src/clis/twitter/notifications.ts +2 -1
  412. package/src/clis/twitter/post.ts +2 -1
  413. package/src/clis/twitter/profile.ts +4 -2
  414. package/src/clis/twitter/reply-dm.ts +2 -1
  415. package/src/clis/twitter/reply.ts +2 -1
  416. package/src/clis/twitter/search.test.ts +113 -0
  417. package/src/clis/twitter/search.ts +38 -14
  418. package/src/clis/twitter/thread.ts +2 -2
  419. package/src/clis/twitter/timeline.ts +3 -2
  420. package/src/clis/twitter/trending.ts +3 -2
  421. package/src/clis/twitter/unblock.ts +2 -1
  422. package/src/clis/twitter/unbookmark.ts +2 -1
  423. package/src/clis/twitter/unfollow.ts +2 -1
  424. package/src/clis/v2ex/daily.ts +3 -2
  425. package/src/clis/v2ex/me.ts +3 -2
  426. package/src/clis/v2ex/notifications.ts +3 -4
  427. package/src/clis/web/read.ts +210 -0
  428. package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
  429. package/src/clis/xueqiu/danjuan-utils.ts +176 -0
  430. package/src/clis/xueqiu/fund-holdings.ts +32 -0
  431. package/src/clis/xueqiu/fund-snapshot.ts +27 -0
  432. package/src/clis/youtube/transcript.ts +5 -4
  433. package/src/clis/youtube/video.ts +3 -2
  434. package/src/daemon.ts +5 -4
  435. package/src/discovery.ts +12 -34
  436. package/src/doctor.ts +3 -2
  437. package/src/download/index.test.ts +93 -2
  438. package/src/download/index.ts +44 -23
  439. package/src/download/media-download.ts +5 -3
  440. package/src/engine.test.ts +84 -3
  441. package/src/execution.ts +62 -46
  442. package/src/explore.ts +21 -90
  443. package/src/external-clis.yaml +0 -8
  444. package/src/external.test.ts +9 -0
  445. package/src/external.ts +12 -10
  446. package/src/generate.ts +4 -41
  447. package/src/hooks.test.ts +126 -0
  448. package/src/hooks.ts +90 -0
  449. package/src/interceptor.ts +73 -23
  450. package/src/main.ts +2 -0
  451. package/src/output.ts +14 -6
  452. package/src/pipeline/executor.ts +1 -1
  453. package/src/pipeline/steps/browser.ts +1 -3
  454. package/src/pipeline/steps/download.test.ts +136 -0
  455. package/src/pipeline/steps/download.ts +47 -34
  456. package/src/pipeline/steps/fetch.test.ts +179 -0
  457. package/src/pipeline/steps/fetch.ts +39 -23
  458. package/src/pipeline/steps/transform.ts +2 -6
  459. package/src/pipeline/template.test.ts +28 -0
  460. package/src/pipeline/template.ts +67 -79
  461. package/src/pipeline/transform.test.ts +20 -0
  462. package/src/plugin.test.ts +251 -3
  463. package/src/plugin.ts +265 -21
  464. package/src/record.ts +12 -84
  465. package/src/registry-api.ts +2 -0
  466. package/src/registry.ts +7 -4
  467. package/src/runtime.ts +14 -4
  468. package/src/snapshotFormatter.ts +43 -121
  469. package/src/utils.ts +39 -0
  470. package/src/validate.ts +3 -5
  471. package/src/yaml-schema.ts +28 -0
  472. package/tests/e2e/browser-auth.test.ts +25 -0
  473. package/tests/e2e/plugin-management.test.ts +137 -0
  474. package/tests/e2e/public-commands.test.ts +34 -1
  475. package/vitest.config.ts +19 -1
  476. package/.github/workflows/pkg-pr-new.yml +0 -30
  477. package/dist/clis/douban/shared.d.ts +0 -4
  478. package/dist/clis/douban/shared.js +0 -155
  479. package/src/clis/douban/shared.ts +0 -165
  480. /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
  481. /package/dist/clis/boss/{common.js → utils.js} +0 -0
  482. /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
  483. /package/dist/clis/doubao/{common.js → utils.js} +0 -0
  484. /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
  485. /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
  486. /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
  487. /package/dist/clis/jike/{shared.js → utils.js} +0 -0
  488. /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
  489. /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
  490. /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
  491. /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
  492. /package/src/clis/boss/{common.ts → utils.ts} +0 -0
  493. /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
  494. /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
  495. /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
  496. /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
@@ -1,21 +1,14 @@
1
1
  /**
2
2
  * Aria snapshot formatter: parses Playwright MCP snapshot text into clean format.
3
3
  *
4
- * Multi-pass pipeline:
5
- * 1. Parse & filter: strip annotations, metadata, noise roles, ads, decorators
6
- * 2. Deduplicate: generic/text child matching parent label
7
- * 3. Deduplicate: heading + link with identical labels
8
- * 4. Deduplicate: nested identical links
9
- * 5. Prune: empty containers (iterative bottom-up)
10
- * 6. Collapse: single-child containers
4
+ * 4-pass pipeline:
5
+ * 1. Parse & filter: strip annotations, metadata, noise, ads, boilerplate subtrees
6
+ * 2. Deduplicate: generic/text parent match, heading+link, nested identical links
7
+ * 3. Prune: empty containers (iterative bottom-up)
8
+ * 4. Collapse: single-child containers
11
9
  */
12
10
 
13
- export interface FormatOptions {
14
- interactive?: boolean;
15
- compact?: boolean;
16
- maxDepth?: number;
17
- maxTextLength?: number;
18
- }
11
+ import type { SnapshotOptions } from './types.js';
19
12
 
20
13
  const DEFAULT_MAX_TEXT_LENGTH = 200;
21
14
 
@@ -199,19 +192,18 @@ interface Entry {
199
192
  trailingText: string;
200
193
  isInteractive: boolean;
201
194
  isLandmark: boolean;
202
- isSubtreeSkip: boolean; // ad nodes or boilerplate — skip entire subtree
203
195
  }
204
196
 
205
- export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
197
+ export function formatSnapshot(raw: string, opts: SnapshotOptions = {}): string {
206
198
  if (!raw || typeof raw !== 'string') return '';
207
199
 
208
200
  const maxTextLen = opts.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH;
209
201
  const lines = raw.split('\n');
210
202
 
211
- // === Pass 1: Parse, filter, and collect entries ===
212
- const entries: Entry[] = [];
203
+ // === Pass 1: Parse, filter, and collect entries (merged with ad/boilerplate subtree skip) ===
204
+ const parsed: Entry[] = [];
213
205
  let refCounter = 0;
214
- let skipUntilDepth = -1; // When >= 0, skip all nodes at depth > this value
206
+ let skipUntilDepth = -1;
215
207
 
216
208
  for (let i = 0; i < lines.length; i++) {
217
209
  const line = lines[i];
@@ -220,148 +212,88 @@ export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
220
212
  const indent = line.length - line.trimStart().length;
221
213
  const depth = Math.floor(indent / 2);
222
214
 
223
- // If we're in a subtree skip zone, check depth
215
+ // Subtree skip zone (noise roles, ads, boilerplate)
224
216
  if (skipUntilDepth >= 0) {
225
- if (depth > skipUntilDepth) continue; // still inside subtree
226
- skipUntilDepth = -1; // exited subtree
217
+ if (depth > skipUntilDepth) continue;
218
+ skipUntilDepth = -1;
227
219
  }
228
220
 
229
221
  let content = line.trimStart();
230
-
231
- // Strip leading "- "
232
- if (content.startsWith('- ')) {
233
- content = content.slice(2);
234
- }
235
-
236
- // Skip metadata lines
222
+ if (content.startsWith('- ')) content = content.slice(2);
237
223
  if (isMetadataLine(content)) continue;
238
-
239
- // Apply maxDepth filter
240
224
  if (opts.maxDepth !== undefined && depth > opts.maxDepth) continue;
241
225
 
242
226
  const { role, text, hasText, trailingText } = parseLine(content);
243
227
 
244
- // Skip noise nodes
245
228
  if (isNoiseNode(role, hasText, text, trailingText)) continue;
246
229
 
247
- // Skip subtree noise roles (contentinfo footer, etc.) — skip entire subtree
248
- if (SUBTREE_NOISE_ROLES.has(role)) {
249
- skipUntilDepth = depth;
250
- continue;
251
- }
230
+ // Subtree noise roles (contentinfo footer, etc.)
231
+ if (SUBTREE_NOISE_ROLES.has(role)) { skipUntilDepth = depth; continue; }
252
232
 
253
- // Strip annotations
254
- content = stripAnnotations(content);
233
+ // Ads and boilerplate — skip entire subtree (merged from old Pass 2)
234
+ if (isAdNode(text, trailingText) || isBoilerplateNode(text)) { skipUntilDepth = depth; continue; }
255
235
 
256
- // Check if node should trigger subtree skip (ads, boilerplate)
257
- const isSubtreeSkip = isAdNode(text, trailingText) || isBoilerplateNode(text);
236
+ content = stripAnnotations(content);
258
237
 
259
- // Interactive mode filter
260
238
  const isInteractive = INTERACTIVE_ROLES.has(role);
261
239
  const isLandmark = LANDMARK_ROLES.has(role);
262
-
263
240
  if (opts.interactive && !isInteractive && !isLandmark && !hasText) continue;
264
241
 
265
- // Compact mode
266
242
  if (opts.compact) {
267
- content = content
268
- .replace(/\s*\[.*?\]\s*/g, ' ')
269
- .replace(/\s+/g, ' ')
270
- .trim();
243
+ content = content.replace(/\s*\[.*?\]\s*/g, ' ').replace(/\s+/g, ' ').trim();
271
244
  }
272
-
273
- // Text truncation
274
245
  if (maxTextLen > 0 && content.length > maxTextLen) {
275
246
  content = content.slice(0, maxTextLen) + '…';
276
247
  }
277
-
278
- // Assign refs to interactive elements
279
248
  if (isInteractive) {
280
249
  refCounter++;
281
250
  content = `[@${refCounter}] ${content}`;
282
251
  }
283
252
 
284
- entries.push({ depth, content, role, text, trailingText, isInteractive, isLandmark, isSubtreeSkip });
285
- }
286
-
287
- // === Pass 2: Remove subtree-skip nodes (ads, boilerplate, contentinfo) ===
288
- let noAds: Entry[] = [];
289
- for (let i = 0; i < entries.length; i++) {
290
- const entry = entries[i];
291
- if (entry.isSubtreeSkip) {
292
- const skipDepth = entry.depth;
293
- i++;
294
- while (i < entries.length && entries[i].depth > skipDepth) {
295
- i++;
296
- }
297
- i--;
298
- continue;
299
- }
300
- noAds.push(entry);
253
+ parsed.push({ depth, content, role, text, trailingText, isInteractive, isLandmark });
301
254
  }
302
255
 
303
- // === Pass 3: Deduplicate child generic/text matching parent label ===
304
- let deduped: Entry[] = [];
305
- for (let i = 0; i < noAds.length; i++) {
306
- const entry = noAds[i];
256
+ // === Pass 2: Deduplicate (merged: generic/text parent match + heading+link + nested links) ===
257
+ const deduped: Entry[] = [];
258
+ for (let i = 0; i < parsed.length; i++) {
259
+ const entry = parsed[i];
307
260
 
261
+ // Dedup: generic/text child matching parent label
308
262
  if (entry.role === 'generic' || entry.role === 'text') {
309
263
  let parent: Entry | undefined;
310
264
  for (let j = deduped.length - 1; j >= 0; j--) {
311
- if (deduped[j].depth < entry.depth) {
312
- parent = deduped[j];
313
- break;
314
- }
265
+ if (deduped[j].depth < entry.depth) { parent = deduped[j]; break; }
315
266
  if (deduped[j].depth === entry.depth) break;
316
267
  }
317
-
318
268
  if (parent) {
319
269
  const childText = entry.trailingText || entry.text;
320
- if (childText && parent.text && childText === parent.text) {
321
- continue;
322
- }
270
+ if (childText && parent.text && childText === parent.text) continue;
323
271
  }
324
272
  }
325
273
 
326
- deduped.push(entry);
327
- }
328
-
329
- // === Pass 4: Deduplicate heading + child link with identical label ===
330
- // Pattern: heading "Title": → link "Title": (same text) → skip the link
331
- const deduped2: Entry[] = [];
332
- for (let i = 0; i < deduped.length; i++) {
333
- const entry = deduped[i];
334
-
274
+ // Dedup: heading + child link with identical label
335
275
  if (entry.role === 'heading' && entry.text) {
336
- const next = deduped[i + 1];
276
+ const next = parsed[i + 1];
337
277
  if (next && next.role === 'link' && next.text === entry.text && next.depth === entry.depth + 1) {
338
- // Keep the heading, skip the link. But preserve link's children re-parented.
339
- deduped2.push(entry);
340
- i++; // skip the link
278
+ deduped.push(entry);
279
+ i++; // skip the link, preserve its children
341
280
  continue;
342
281
  }
343
282
  }
344
283
 
345
- deduped2.push(entry);
346
- }
347
-
348
- // === Pass 5: Deduplicate nested identical links ===
349
- const deduped3: Entry[] = [];
350
- for (let i = 0; i < deduped2.length; i++) {
351
- const entry = deduped2[i];
352
-
284
+ // Dedup: nested identical links (skip parent, keep child)
353
285
  if (entry.role === 'link' && entry.text) {
354
- const next = deduped2[i + 1];
286
+ const next = parsed[i + 1];
355
287
  if (next && next.role === 'link' && next.text === entry.text && next.depth === entry.depth + 1) {
356
- continue; // Skip parent, keep child
288
+ continue;
357
289
  }
358
290
  }
359
291
 
360
- deduped3.push(entry);
292
+ deduped.push(entry);
361
293
  }
362
294
 
363
- // === Pass 6: Iteratively prune empty containers (bottom-up) ===
364
- let current = deduped3;
295
+ // === Pass 3: Iteratively prune empty containers (bottom-up) ===
296
+ let current = deduped;
365
297
  let changed = true;
366
298
  while (changed) {
367
299
  changed = false;
@@ -372,22 +304,16 @@ export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
372
304
  let hasChildren = false;
373
305
  for (let j = i + 1; j < current.length; j++) {
374
306
  if (current[j].depth <= entry.depth) break;
375
- if (current[j].depth > entry.depth) {
376
- hasChildren = true;
377
- break;
378
- }
379
- }
380
- if (!hasChildren) {
381
- changed = true;
382
- continue;
307
+ if (current[j].depth > entry.depth) { hasChildren = true; break; }
383
308
  }
309
+ if (!hasChildren) { changed = true; continue; }
384
310
  }
385
311
  next.push(entry);
386
312
  }
387
313
  current = next;
388
314
  }
389
315
 
390
- // === Pass 7: Collapse single-child containers ===
316
+ // === Pass 4: Collapse single-child containers ===
391
317
  const collapsed: Entry[] = [];
392
318
  for (let i = 0; i < current.length; i++) {
393
319
  const entry = current[i];
@@ -408,17 +334,13 @@ export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
408
334
  let hasGrandchildren = false;
409
335
  for (let j = childIdx + 1; j < current.length; j++) {
410
336
  if (current[j].depth <= child.depth) break;
411
- if (current[j].depth > child.depth) {
412
- hasGrandchildren = true;
413
- break;
414
- }
337
+ if (current[j].depth > child.depth) { hasGrandchildren = true; break; }
415
338
  }
416
339
 
417
340
  if (!hasGrandchildren) {
418
- const mergedContent = entry.content.replace(/:$/, '') + ' > ' + child.content;
419
341
  collapsed.push({
420
342
  ...entry,
421
- content: mergedContent,
343
+ content: entry.content.replace(/:$/, '') + ' > ' + child.content,
422
344
  role: child.role,
423
345
  text: child.text,
424
346
  trailingText: child.trailingText,
package/src/utils.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared utility functions used across the codebase.
3
+ */
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+
8
+ /** Type guard: checks if a value is a non-null, non-array object. */
9
+ export function isRecord(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ /** Simple async concurrency limiter. */
14
+ export async function mapConcurrent<T, R>(
15
+ items: T[],
16
+ limit: number,
17
+ fn: (item: T, index: number) => Promise<R>,
18
+ ): Promise<R[]> {
19
+ const results: R[] = new Array(items.length);
20
+ let index = 0;
21
+
22
+ async function worker() {
23
+ while (index < items.length) {
24
+ const i = index++;
25
+ results[i] = await fn(items[i], i);
26
+ }
27
+ }
28
+
29
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
30
+ await Promise.all(workers);
31
+ return results;
32
+ }
33
+
34
+ /** Save a base64-encoded string to a file, creating parent directories as needed. */
35
+ export async function saveBase64ToFile(base64: string, filePath: string): Promise<void> {
36
+ const dir = path.dirname(filePath);
37
+ await fs.promises.mkdir(dir, { recursive: true });
38
+ await fs.promises.writeFile(filePath, Buffer.from(base64, 'base64'));
39
+ }
package/src/validate.ts CHANGED
@@ -6,10 +6,10 @@ import { getErrorMessage } from './errors.js';
6
6
 
7
7
  /** All recognized pipeline step names */
8
8
  const KNOWN_STEP_NAMES = new Set([
9
- 'navigate', 'click', 'type', 'wait', 'press', 'snapshot', 'scroll',
9
+ 'navigate', 'click', 'type', 'wait', 'press', 'snapshot',
10
10
  'fetch', 'evaluate',
11
11
  'select', 'map', 'filter', 'sort', 'limit',
12
- 'intercept', 'tap',
12
+ 'intercept', 'tap', 'download',
13
13
  ]);
14
14
 
15
15
  export interface FileValidationResult {
@@ -34,9 +34,7 @@ interface ValidatedYamlCliDefinition {
34
34
  args?: Record<string, unknown>;
35
35
  }
36
36
 
37
- function isRecord(value: unknown): value is Record<string, unknown> {
38
- return typeof value === 'object' && value !== null && !Array.isArray(value);
39
- }
37
+ import { isRecord } from './utils.js';
40
38
 
41
39
 
42
40
  export function validateClisWithTarget(dirs: string[], target?: string): ValidationReport {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared YAML CLI definition types.
3
+ * Used by both discovery.ts (runtime) and build-manifest.ts (build-time).
4
+ */
5
+
6
+ export interface YamlArgDefinition {
7
+ type?: string;
8
+ default?: unknown;
9
+ required?: boolean;
10
+ positional?: boolean;
11
+ description?: string;
12
+ help?: string;
13
+ choices?: string[];
14
+ }
15
+
16
+ export interface YamlCliDefinition {
17
+ site?: string;
18
+ name?: string;
19
+ description?: string;
20
+ domain?: string;
21
+ strategy?: string;
22
+ browser?: boolean;
23
+ args?: Record<string, YamlArgDefinition>;
24
+ columns?: string[];
25
+ pipeline?: Record<string, unknown>[];
26
+ timeout?: number;
27
+ navigateBefore?: boolean | string;
28
+ }
@@ -113,6 +113,31 @@ describe('login-required commands — graceful failure', () => {
113
113
  await expectGracefulAuthFailure(['xiaohongshu', 'notifications', '--limit', '3', '-f', 'json'], 'xiaohongshu notifications');
114
114
  }, 60_000);
115
115
 
116
+ // ── pixiv (requires login) ──
117
+ it('pixiv ranking fails gracefully without login', async () => {
118
+ await expectGracefulAuthFailure(['pixiv', 'ranking', '--limit', '3', '-f', 'json'], 'pixiv ranking');
119
+ }, 60_000);
120
+
121
+ it('pixiv search fails gracefully without login', async () => {
122
+ await expectGracefulAuthFailure(['pixiv', 'search', '初音ミク', '--limit', '3', '-f', 'json'], 'pixiv search');
123
+ }, 60_000);
124
+
125
+ it('pixiv user fails gracefully without login', async () => {
126
+ await expectGracefulAuthFailure(['pixiv', 'user', '11', '-f', 'json'], 'pixiv user');
127
+ }, 60_000);
128
+
129
+ it('pixiv illusts fails gracefully without login', async () => {
130
+ await expectGracefulAuthFailure(['pixiv', 'illusts', '11', '--limit', '3', '-f', 'json'], 'pixiv illusts');
131
+ }, 60_000);
132
+
133
+ it('pixiv detail fails gracefully without login', async () => {
134
+ await expectGracefulAuthFailure(['pixiv', 'detail', '123456', '-f', 'json'], 'pixiv detail');
135
+ }, 60_000);
136
+
137
+ it('pixiv download fails gracefully without login', async () => {
138
+ await expectGracefulAuthFailure(['pixiv', 'download', '123456', '--output', '/tmp/pixiv-e2e-test', '-f', 'json'], 'pixiv download');
139
+ }, 60_000);
140
+
116
141
  // ── yollomi (requires login session) ──
117
142
  it('yollomi generate fails gracefully without login', async () => {
118
143
  await expectGracefulAuthFailure(['yollomi', 'generate', 'a cute cat', '--no-download', '-f', 'json'], 'yollomi generate');
@@ -0,0 +1,137 @@
1
+ /**
2
+ * E2E integration tests for plugin management commands.
3
+ * Uses a real GitHub plugin (opencli-plugin-hot-digest) to verify the full
4
+ * install → list → update → uninstall lifecycle in an isolated HOME.
5
+ */
6
+
7
+ import { describe, it, expect, afterAll } from 'vitest';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import { runCli, parseJsonOutput } from './helpers.js';
12
+
13
+ const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-plugin-e2e-'));
14
+ const OPENCLI_HOME = path.join(TEST_HOME, '.opencli');
15
+ const PLUGINS_DIR = path.join(OPENCLI_HOME, 'plugins');
16
+ const PLUGIN_SOURCE = 'github:ByteYue/opencli-plugin-hot-digest';
17
+ const PLUGIN_NAME = 'hot-digest';
18
+ const PLUGIN_DIR = path.join(PLUGINS_DIR, PLUGIN_NAME);
19
+ const LOCK_FILE = path.join(OPENCLI_HOME, 'plugins.lock.json');
20
+
21
+ function runPluginCli(
22
+ args: string[],
23
+ opts: { timeout?: number; env?: Record<string, string> } = {},
24
+ ) {
25
+ return runCli(args, {
26
+ ...opts,
27
+ env: {
28
+ HOME: TEST_HOME,
29
+ USERPROFILE: TEST_HOME,
30
+ ...opts.env,
31
+ },
32
+ });
33
+ }
34
+
35
+ describe('plugin management E2E', () => {
36
+ afterAll(() => {
37
+ fs.rmSync(TEST_HOME, { recursive: true, force: true });
38
+ });
39
+
40
+ // ── plugin list (empty) ──
41
+ it('plugin list shows "No plugins installed" when none exist', async () => {
42
+ const { stdout, code } = await runPluginCli(['plugin', 'list']);
43
+ expect(code).toBe(0);
44
+ expect(stdout).toContain('No plugins installed');
45
+ });
46
+
47
+ // ── plugin install ──
48
+ it('plugin install clones and sets up a real plugin', async () => {
49
+ const { stdout, code } = await runPluginCli(['plugin', 'install', PLUGIN_SOURCE], {
50
+ timeout: 60_000,
51
+ });
52
+ expect(code).toBe(0);
53
+ expect(stdout).toContain('installed successfully');
54
+ expect(stdout).toContain(PLUGIN_NAME);
55
+
56
+ // Verify the plugin directory was created
57
+ expect(fs.existsSync(PLUGIN_DIR)).toBe(true);
58
+
59
+ // Verify lock file was updated
60
+ const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
61
+ expect(lock[PLUGIN_NAME]).toBeDefined();
62
+ expect(lock[PLUGIN_NAME].commitHash).toBeTruthy();
63
+ expect(lock[PLUGIN_NAME].source).toContain('opencli-plugin-hot-digest');
64
+ expect(lock[PLUGIN_NAME].installedAt).toBeTruthy();
65
+ }, 60_000);
66
+
67
+ // ── plugin list (after install) ──
68
+ it('plugin list shows the installed plugin', async () => {
69
+ const { stdout, code } = await runPluginCli(['plugin', 'list']);
70
+ expect(code).toBe(0);
71
+ expect(stdout).toContain(PLUGIN_NAME);
72
+ });
73
+
74
+ it('plugin list -f json returns structured data', async () => {
75
+ const { stdout, code } = await runPluginCli(['plugin', 'list', '-f', 'json']);
76
+ expect(code).toBe(0);
77
+ const data = parseJsonOutput(stdout);
78
+ expect(Array.isArray(data)).toBe(true);
79
+
80
+ const plugin = data.find((p: any) => p.name === PLUGIN_NAME);
81
+ expect(plugin).toBeDefined();
82
+ expect(plugin.name).toBe(PLUGIN_NAME);
83
+ expect(Array.isArray(plugin.commands)).toBe(true);
84
+ expect(plugin.commands.length).toBeGreaterThan(0);
85
+ });
86
+
87
+ // ── plugin update ──
88
+ it('plugin update succeeds on an installed plugin', async () => {
89
+ const { stdout, code } = await runPluginCli(['plugin', 'update', PLUGIN_NAME], {
90
+ timeout: 30_000,
91
+ });
92
+ expect(code).toBe(0);
93
+ expect(stdout).toContain('updated successfully');
94
+
95
+ // Verify lock file has updatedAt
96
+ const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
97
+ expect(lock[PLUGIN_NAME].updatedAt).toBeTruthy();
98
+ }, 30_000);
99
+
100
+ // ── plugin uninstall ──
101
+ it('plugin uninstall removes the plugin', async () => {
102
+ const { stdout, code } = await runPluginCli(['plugin', 'uninstall', PLUGIN_NAME]);
103
+ expect(code).toBe(0);
104
+ expect(stdout).toContain('uninstalled');
105
+
106
+ // Verify directory was removed
107
+ expect(fs.existsSync(PLUGIN_DIR)).toBe(false);
108
+
109
+ // Verify lock entry was removed
110
+ const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
111
+ expect(lock[PLUGIN_NAME]).toBeUndefined();
112
+ });
113
+
114
+ // ── error paths ──
115
+ it('plugin install rejects invalid source', async () => {
116
+ const { stderr, code } = await runPluginCli(['plugin', 'install', 'invalid-source-format']);
117
+ expect(code).toBe(1);
118
+ expect(stderr).toContain('Invalid plugin source');
119
+ });
120
+
121
+ it('plugin uninstall rejects non-existent plugin', async () => {
122
+ const { stderr, code } = await runPluginCli(['plugin', 'uninstall', '__nonexistent_plugin_xyz__']);
123
+ expect(code).toBe(1);
124
+ expect(stderr).toContain('not installed');
125
+ });
126
+
127
+ it('plugin update rejects non-existent plugin', async () => {
128
+ const { stderr, code } = await runPluginCli(['plugin', 'update', '__nonexistent_plugin_xyz__']);
129
+ expect(code).toBe(1);
130
+ });
131
+
132
+ it('plugin update without name or --all shows error', async () => {
133
+ const { stderr, code } = await runPluginCli(['plugin', 'update']);
134
+ expect(code).toBe(1);
135
+ expect(stderr).toContain('specify a plugin name');
136
+ });
137
+ });
@@ -16,7 +16,8 @@ function isExpectedChineseSiteRestriction(code: number, stderr: string): boolean
16
16
 
17
17
  function isExpectedApplePodcastsRestriction(code: number, stderr: string): boolean {
18
18
  if (code === 0) return false;
19
- return /Error \[FETCH_ERROR\]: (Charts API HTTP \d+|Unable to reach Apple Podcasts charts)/.test(stderr);
19
+ return /Error \[FETCH_ERROR\]: (Charts API HTTP \d+|Unable to reach Apple Podcasts charts)/.test(stderr)
20
+ || stderr === ''; // timeout killed the process before any output
20
21
  }
21
22
 
22
23
  function isExpectedGoogleRestriction(code: number, stderr: string): boolean {
@@ -488,4 +489,36 @@ describe('public commands E2E', () => {
488
489
  expect(data.length).toBeGreaterThan(0);
489
490
  expect(data.every((d: any) => d.type === 'image')).toBe(true);
490
491
  }, 30_000);
492
+
493
+ // ── dictionary (public API, browser: false) ──
494
+ it('dictionary search returns word definitions', async () => {
495
+ const { stdout, code } = await runCli(['dictionary', 'search', 'serendipity', '-f', 'json']);
496
+ expect(code).toBe(0);
497
+ const data = parseJsonOutput(stdout);
498
+ expect(Array.isArray(data)).toBe(true);
499
+ expect(data.length).toBeGreaterThanOrEqual(1);
500
+ expect(data[0]).toHaveProperty('word', 'serendipity');
501
+ expect(data[0]).toHaveProperty('phonetic');
502
+ expect(data[0]).toHaveProperty('definition');
503
+ }, 30_000);
504
+
505
+ it('dictionary synonyms returns synonyms', async () => {
506
+ const { stdout, code } = await runCli(['dictionary', 'synonyms', 'serendipity', '-f', 'json']);
507
+ expect(code).toBe(0);
508
+ const data = parseJsonOutput(stdout);
509
+ expect(Array.isArray(data)).toBe(true);
510
+ expect(data.length).toBeGreaterThanOrEqual(1);
511
+ expect(data[0]).toHaveProperty('word', 'serendipity');
512
+ expect(data[0]).toHaveProperty('synonyms');
513
+ }, 30_000);
514
+
515
+ it('dictionary examples returns examples', async () => {
516
+ const { stdout, code } = await runCli(['dictionary', 'examples', 'perfect', '-f', 'json']);
517
+ expect(code).toBe(0);
518
+ const data = parseJsonOutput(stdout);
519
+ expect(Array.isArray(data)).toBe(true);
520
+ expect(data.length).toBeGreaterThanOrEqual(1);
521
+ expect(data[0]).toHaveProperty('word', 'perfect');
522
+ expect(data[0]).toHaveProperty('example');
523
+ }, 30_000);
491
524
  });
package/vitest.config.ts CHANGED
@@ -7,19 +7,37 @@ export default defineConfig({
7
7
  test: {
8
8
  name: 'unit',
9
9
  include: ['src/**/*.test.ts'],
10
+ exclude: ['src/clis/**/*.test.ts'],
10
11
  // Run unit tests before e2e tests to avoid project-level contention in CI.
11
12
  sequence: {
12
13
  groupOrder: 0,
13
14
  },
14
15
  },
15
16
  },
17
+ {
18
+ test: {
19
+ name: 'adapter',
20
+ include: [
21
+ 'src/clis/zhihu/**/*.test.ts',
22
+ 'src/clis/twitter/**/*.test.ts',
23
+ 'src/clis/reddit/**/*.test.ts',
24
+ 'src/clis/bilibili/**/*.test.ts',
25
+ 'src/clis/linkedin/**/*.test.ts',
26
+ 'src/clis/grok/**/*.test.ts',
27
+ 'src/clis/pixiv/**/*.test.ts',
28
+ ],
29
+ sequence: {
30
+ groupOrder: 1,
31
+ },
32
+ },
33
+ },
16
34
  {
17
35
  test: {
18
36
  name: 'e2e',
19
37
  include: ['tests/**/*.test.ts'],
20
38
  maxWorkers: 2,
21
39
  sequence: {
22
- groupOrder: 1,
40
+ groupOrder: 2,
23
41
  },
24
42
  },
25
43
  },
@@ -1,30 +0,0 @@
1
- name: Publish Any Commit
2
-
3
- on:
4
- push:
5
- branches: [main, dev]
6
- pull_request:
7
- branches: [main, dev]
8
-
9
- permissions: {}
10
-
11
- jobs:
12
- publish:
13
- if: ${{ vars.PKG_PR_NEW_ENABLED == 'true' }}
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v6
17
-
18
- - uses: actions/setup-node@v6
19
- with:
20
- node-version: '22'
21
- cache: 'npm'
22
-
23
- - name: Install dependencies
24
- run: npm ci
25
-
26
- - name: Build
27
- run: npm run build
28
-
29
- - name: Publish to pkg.pr.new
30
- run: npx pkg-pr-new publish
@@ -1,4 +0,0 @@
1
- import type { IPage } from '../../types.js';
2
- export declare function loadDoubanBookHot(page: IPage, limit: number): Promise<any[]>;
3
- export declare function loadDoubanMovieHot(page: IPage, limit: number): Promise<any[]>;
4
- export declare function searchDouban(page: IPage, type: string, keyword: string, limit: number): Promise<any[]>;