@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
@@ -2,6 +2,8 @@
2
2
  * Pipeline template engine: ${{ ... }} expression rendering.
3
3
  */
4
4
 
5
+ import vm from 'node:vm';
6
+
5
7
  export interface RenderContext {
6
8
  args?: Record<string, unknown>;
7
9
  data?: unknown;
@@ -9,9 +11,7 @@ export interface RenderContext {
9
11
  index?: number;
10
12
  }
11
13
 
12
- function isRecord(value: unknown): value is Record<string, unknown> {
13
- return typeof value === 'object' && value !== null && !Array.isArray(value);
14
- }
14
+ import { isRecord } from '../utils.js';
15
15
 
16
16
  export function render(template: unknown, ctx: RenderContext): unknown {
17
17
  if (typeof template !== 'string') return template;
@@ -37,45 +37,29 @@ export function evalExpr(expr: string, ctx: RenderContext): unknown {
37
37
  const index = ctx.index ?? 0;
38
38
 
39
39
  // ── Pipe filters: expr | filter1(arg) | filter2 ──
40
- // Supports: default(val), join(sep), upper, lower, truncate(n), trim, replace(old,new)
41
- if (expr.includes('|') && !expr.includes('||')) {
42
- const segments = expr.split('|').map(s => s.trim());
43
- const mainExpr = segments[0];
44
- let result = resolvePath(mainExpr, { args, item, data, index });
45
- for (let i = 1; i < segments.length; i++) {
46
- result = applyFilter(segments[i], result);
40
+ // Split on single | (not ||) so "item.a || item.b | upper" works correctly.
41
+ const pipeSegments = expr.split(/(?<!\|)\|(?!\|)/).map(s => s.trim());
42
+ if (pipeSegments.length > 1) {
43
+ let result = evalExpr(pipeSegments[0], ctx);
44
+ for (let i = 1; i < pipeSegments.length; i++) {
45
+ result = applyFilter(pipeSegments[i], result);
47
46
  }
48
47
  return result;
49
48
  }
50
49
 
51
- // Arithmetic: index + 1
52
- const arithMatch = expr.match(/^([\w][\w.]*)\s*([+\-*/])\s*(\d+)$/);
53
- if (arithMatch) {
54
- const [, varName, op, numStr] = arithMatch;
55
- const val = resolvePath(varName, { args, item, data, index });
56
- if (val !== null && val !== undefined) {
57
- const numVal = Number(val); const num = Number(numStr);
58
- if (!isNaN(numVal)) {
59
- switch (op) {
60
- case '+': return numVal + num; case '-': return numVal - num;
61
- case '*': return numVal * num; case '/': return num !== 0 ? numVal / num : 0;
62
- }
63
- }
64
- }
65
- }
50
+ // Fast path: quoted string literal — skip VM overhead
51
+ const strLit = expr.match(/^(['"])(.*)\1$/);
52
+ if (strLit) return strLit[2];
66
53
 
67
- // JS-like fallback expression: item.tweetCount || 'N/A'
68
- const orMatch = expr.match(/^(.+?)\s*\|\|\s*(.+)$/);
69
- if (orMatch) {
70
- const left = evalExpr(orMatch[1].trim(), ctx);
71
- if (left) return left;
72
- const right = orMatch[2].trim();
73
- return right.replace(/^['"]|['"]$/g, '');
74
- }
54
+ // Fast path: numeric literal
55
+ if (/^\d+(\.\d+)?$/.test(expr)) return Number(expr);
75
56
 
57
+ // Try resolving as a simple dotted path (item.foo.bar, args.limit, index)
76
58
  const resolved = resolvePath(expr, { args, item, data, index });
77
59
  if (resolved !== null && resolved !== undefined) return resolved;
78
60
 
61
+ // Fallback: evaluate as JS in a sandboxed VM.
62
+ // Handles ||, ??, arithmetic, ternary, method calls, etc. natively.
79
63
  return evalJsExpr(expr, { args, item, data, index });
80
64
  }
81
65
 
@@ -95,7 +79,7 @@ function applyFilter(filterExpr: string, value: unknown): unknown {
95
79
  case 'default': {
96
80
  if (value === null || value === undefined || value === '') {
97
81
  const intVal = parseInt(filterArg, 10);
98
- if (!isNaN(intVal) && String(intVal) === filterArg.trim()) return intVal;
82
+ if (!Number.isNaN(intVal) && String(intVal) === filterArg.trim()) return intVal;
99
83
  return filterArg;
100
84
  }
101
85
  return value;
@@ -110,7 +94,7 @@ function applyFilter(filterExpr: string, value: unknown): unknown {
110
94
  return typeof value === 'string' ? value.trim() : value;
111
95
  case 'truncate': {
112
96
  const n = parseInt(filterArg, 10) || 50;
113
- return typeof value === 'string' && value.length > n ? value.slice(0, n) + '...' : value;
97
+ return typeof value === 'string' && value.length > n ? `${value.slice(0, n)}...` : value;
114
98
  }
115
99
  case 'replace': {
116
100
  if (typeof value !== 'string') return value;
@@ -138,6 +122,7 @@ function applyFilter(filterExpr: string, value: unknown): unknown {
138
122
  case 'sanitize':
139
123
  // Remove invalid filename characters
140
124
  return typeof value === 'string'
125
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional - strips C0 control chars from filenames
141
126
  ? value.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
142
127
  : value;
143
128
  case 'ext': {
@@ -186,58 +171,61 @@ export function resolvePath(pathStr: string, ctx: RenderContext): unknown {
186
171
 
187
172
  /**
188
173
  * Evaluate arbitrary JS expressions as a last-resort fallback.
189
- *
190
- * ⚠️ SECURITY NOTE: Uses `new Function()` to execute the expression.
191
- * This is acceptable here because:
192
- * 1. YAML adapters are authored by trusted repo contributors only.
193
- * 2. The expression runs in the same Node.js process (no sandbox).
194
- * 3. Only a curated set of globals is exposed (no require/import/process/fs).
195
- * If opencli ever loads untrusted third-party adapters, this MUST be replaced
196
- * with a proper sandboxed evaluator.
174
+ * Runs inside a `node:vm` sandbox with dynamic code generation disabled.
197
175
  */
176
+ const FORBIDDEN_EXPR_PATTERNS = /\b(constructor|__proto__|prototype|globalThis|process|require|import|eval)\b/;
177
+
178
+ /**
179
+ * Deep-copy plain data to sever prototype chains, preventing sandbox escape
180
+ * via `args.constructor.constructor('return process')()` etc.
181
+ */
182
+ function sanitizeContext(obj: unknown): unknown {
183
+ if (obj === null || obj === undefined) return obj;
184
+ if (typeof obj !== 'object' && typeof obj !== 'function') return obj;
185
+ try {
186
+ return JSON.parse(JSON.stringify(obj));
187
+ } catch {
188
+ return {};
189
+ }
190
+ }
191
+
198
192
  function evalJsExpr(expr: string, ctx: RenderContext): unknown {
199
193
  // Guard against absurdly long expressions that could indicate injection.
200
194
  if (expr.length > 2000) return undefined;
201
195
 
202
- const args = ctx.args ?? {};
203
- const item = ctx.item ?? {};
204
- const data = ctx.data;
196
+ // Block obvious sandbox escape attempts.
197
+ if (FORBIDDEN_EXPR_PATTERNS.test(expr)) return undefined;
198
+
199
+ const args = sanitizeContext(ctx.args ?? {});
200
+ const item = sanitizeContext(ctx.item ?? {});
201
+ const data = sanitizeContext(ctx.data);
205
202
  const index = ctx.index ?? 0;
206
203
 
207
204
  try {
208
- const fn = new Function(
209
- 'args',
210
- 'item',
211
- 'data',
212
- 'index',
213
- 'encodeURIComponent',
214
- 'decodeURIComponent',
215
- 'JSON',
216
- 'Math',
217
- 'Number',
218
- 'String',
219
- 'Boolean',
220
- 'Array',
221
- 'Object',
222
- 'Date',
223
- `"use strict"; return (${expr});`,
224
- );
225
-
226
- return fn(
227
- args,
228
- item,
229
- data,
230
- index,
231
- encodeURIComponent,
232
- decodeURIComponent,
233
- JSON,
234
- Math,
235
- Number,
236
- String,
237
- Boolean,
238
- Array,
239
- Object,
240
- Date,
205
+ return vm.runInNewContext(
206
+ `(${expr})`,
207
+ {
208
+ args,
209
+ item,
210
+ data,
211
+ index,
212
+ encodeURIComponent,
213
+ decodeURIComponent,
214
+ JSON,
215
+ Math,
216
+ Number,
217
+ String,
218
+ Boolean,
219
+ Array,
220
+ Date,
221
+ },
222
+ {
223
+ timeout: 50,
224
+ contextCodeGeneration: {
225
+ strings: false,
226
+ wasm: false,
227
+ },
228
+ },
241
229
  );
242
230
  } catch {
243
231
  return undefined;
@@ -101,6 +101,26 @@ describe('stepSort', () => {
101
101
  await stepSort(null, 'score', SAMPLE_DATA, {});
102
102
  expect(SAMPLE_DATA).toEqual(original);
103
103
  });
104
+
105
+ it('sorts string-encoded numbers naturally by default', async () => {
106
+ const data = [
107
+ { name: 'A', volume: '99' },
108
+ { name: 'B', volume: '1000' },
109
+ { name: 'C', volume: '250' },
110
+ ];
111
+ const result = await stepSort(null, { by: 'volume', order: 'desc' }, data, {});
112
+ expect((result as typeof data).map((r) => r.name)).toEqual(['B', 'C', 'A']);
113
+ });
114
+
115
+ it('handles missing fields gracefully', async () => {
116
+ const data = [
117
+ { name: 'A', value: '10' },
118
+ { name: 'B' },
119
+ { name: 'C', value: '5' },
120
+ ];
121
+ const result = await stepSort(null, { by: 'value', order: 'asc' }, data, {});
122
+ expect((result as typeof data).map((r) => r.name)).toEqual(['B', 'C', 'A']);
123
+ });
104
124
  });
105
125
 
106
126
  describe('stepLimit', () => {
@@ -1,12 +1,28 @@
1
1
  /**
2
- * Tests for plugin management: install, uninstall, list.
2
+ * Tests for plugin management: install, uninstall, list, and lock file support.
3
3
  */
4
4
 
5
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
6
  import * as fs from 'node:fs';
7
+ import * as os from 'node:os';
7
8
  import * as path from 'node:path';
8
9
  import { PLUGINS_DIR } from './discovery.js';
9
- import { listPlugins, uninstallPlugin, updatePlugin, _parseSource } from './plugin.js';
10
+ import type { LockEntry } from './plugin.js';
11
+ import * as pluginModule from './plugin.js';
12
+
13
+ const {
14
+ LOCK_FILE,
15
+ _getCommitHash,
16
+ listPlugins,
17
+ _readLockFile,
18
+ _resolveEsbuildBin,
19
+ uninstallPlugin,
20
+ updatePlugin,
21
+ _parseSource,
22
+ _updateAllPlugins,
23
+ _validatePluginStructure,
24
+ _writeLockFile,
25
+ } = pluginModule;
10
26
 
11
27
  describe('parseSource', () => {
12
28
  it('parses github:user/repo format', () => {
@@ -41,6 +57,142 @@ describe('parseSource', () => {
41
57
  });
42
58
  });
43
59
 
60
+ describe('validatePluginStructure', () => {
61
+ const testDir = path.join(PLUGINS_DIR, '__test-validate__');
62
+
63
+ beforeEach(() => {
64
+ fs.mkdirSync(testDir, { recursive: true });
65
+ });
66
+
67
+ afterEach(() => {
68
+ try { fs.rmSync(testDir, { recursive: true }); } catch {}
69
+ });
70
+
71
+ it('returns invalid for non-existent directory', () => {
72
+ const res = _validatePluginStructure(path.join(PLUGINS_DIR, '__does_not_exist__'));
73
+ expect(res.valid).toBe(false);
74
+ expect(res.errors[0]).toContain('does not exist');
75
+ });
76
+
77
+ it('returns invalid for empty directory', () => {
78
+ const res = _validatePluginStructure(testDir);
79
+ expect(res.valid).toBe(false);
80
+ expect(res.errors[0]).toContain('No command files found');
81
+ });
82
+
83
+ it('returns valid for YAML plugin', () => {
84
+ fs.writeFileSync(path.join(testDir, 'cmd.yaml'), 'site: test');
85
+ const res = _validatePluginStructure(testDir);
86
+ expect(res.valid).toBe(true);
87
+ expect(res.errors).toHaveLength(0);
88
+ });
89
+
90
+ it('returns valid for JS plugin', () => {
91
+ fs.writeFileSync(path.join(testDir, 'cmd.js'), 'console.log("hi");');
92
+ const res = _validatePluginStructure(testDir);
93
+ expect(res.valid).toBe(true);
94
+ expect(res.errors).toHaveLength(0);
95
+ });
96
+
97
+ it('returns invalid for TS plugin without package.json', () => {
98
+ fs.writeFileSync(path.join(testDir, 'cmd.ts'), 'console.log("hi");');
99
+ const res = _validatePluginStructure(testDir);
100
+ expect(res.valid).toBe(false);
101
+ expect(res.errors[0]).toContain('contains .ts files but no package.json');
102
+ });
103
+
104
+ it('returns invalid for TS plugin with missing type: module', () => {
105
+ fs.writeFileSync(path.join(testDir, 'cmd.ts'), 'console.log("hi");');
106
+ fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test' }));
107
+ const res = _validatePluginStructure(testDir);
108
+ expect(res.valid).toBe(false);
109
+ expect(res.errors[0]).toContain('must have "type": "module"');
110
+ });
111
+
112
+ it('returns valid for TS plugin with correct package.json', () => {
113
+ fs.writeFileSync(path.join(testDir, 'cmd.ts'), 'console.log("hi");');
114
+ fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ type: 'module' }));
115
+ const res = _validatePluginStructure(testDir);
116
+ expect(res.valid).toBe(true);
117
+ expect(res.errors).toHaveLength(0);
118
+ });
119
+ });
120
+
121
+ describe('lock file', () => {
122
+ const backupPath = `${LOCK_FILE}.test-backup`;
123
+ let hadOriginal = false;
124
+
125
+ beforeEach(() => {
126
+ hadOriginal = fs.existsSync(LOCK_FILE);
127
+ if (hadOriginal) {
128
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
129
+ fs.copyFileSync(LOCK_FILE, backupPath);
130
+ }
131
+ });
132
+
133
+ afterEach(() => {
134
+ if (hadOriginal) {
135
+ fs.copyFileSync(backupPath, LOCK_FILE);
136
+ fs.unlinkSync(backupPath);
137
+ return;
138
+ }
139
+ try { fs.unlinkSync(LOCK_FILE); } catch {}
140
+ });
141
+
142
+ it('reads empty lock when file does not exist', () => {
143
+ try { fs.unlinkSync(LOCK_FILE); } catch {}
144
+ expect(_readLockFile()).toEqual({});
145
+ });
146
+
147
+ it('round-trips lock entries', () => {
148
+ const entries: Record<string, LockEntry> = {
149
+ 'test-plugin': {
150
+ source: 'https://github.com/user/repo.git',
151
+ commitHash: 'abc1234567890def',
152
+ installedAt: '2025-01-01T00:00:00.000Z',
153
+ },
154
+ 'another-plugin': {
155
+ source: 'https://github.com/user/another.git',
156
+ commitHash: 'def4567890123abc',
157
+ installedAt: '2025-02-01T00:00:00.000Z',
158
+ updatedAt: '2025-03-01T00:00:00.000Z',
159
+ },
160
+ };
161
+
162
+ _writeLockFile(entries);
163
+ expect(_readLockFile()).toEqual(entries);
164
+ });
165
+
166
+ it('handles malformed lock file gracefully', () => {
167
+ fs.mkdirSync(path.dirname(LOCK_FILE), { recursive: true });
168
+ fs.writeFileSync(LOCK_FILE, 'not valid json');
169
+ expect(_readLockFile()).toEqual({});
170
+ });
171
+ });
172
+
173
+ describe('getCommitHash', () => {
174
+ it('returns a hash for a git repo', () => {
175
+ const hash = _getCommitHash(process.cwd());
176
+ expect(hash).toBeDefined();
177
+ expect(hash).toMatch(/^[0-9a-f]{40}$/);
178
+ });
179
+
180
+ it('returns undefined for non-git directory', () => {
181
+ expect(_getCommitHash(os.tmpdir())).toBeUndefined();
182
+ });
183
+ });
184
+
185
+ describe('resolveEsbuildBin', () => {
186
+ it('resolves a usable esbuild executable path', () => {
187
+ const binPath = _resolveEsbuildBin();
188
+ expect(binPath).not.toBeNull();
189
+ expect(typeof binPath).toBe('string');
190
+ expect(fs.existsSync(binPath!)).toBe(true);
191
+ // On Windows the resolved path ends with 'esbuild.cmd', on Unix 'esbuild'
192
+ expect(binPath).toMatch(/esbuild(\.cmd)?$/);
193
+ });
194
+ });
195
+
44
196
  describe('listPlugins', () => {
45
197
  const testDir = path.join(PLUGINS_DIR, '__test-list-plugin__');
46
198
 
@@ -58,6 +210,28 @@ describe('listPlugins', () => {
58
210
  expect(found!.commands).toContain('hello');
59
211
  });
60
212
 
213
+ it('includes version metadata from the lock file', () => {
214
+ fs.mkdirSync(testDir, { recursive: true });
215
+ fs.writeFileSync(path.join(testDir, 'hello.yaml'), 'site: test\nname: hello\n');
216
+
217
+ const lock = _readLockFile();
218
+ lock['__test-list-plugin__'] = {
219
+ source: 'https://github.com/user/repo.git',
220
+ commitHash: 'abcdef1234567890abcdef1234567890abcdef12',
221
+ installedAt: '2025-01-01T00:00:00.000Z',
222
+ };
223
+ _writeLockFile(lock);
224
+
225
+ const plugins = listPlugins();
226
+ const found = plugins.find(p => p.name === '__test-list-plugin__');
227
+ expect(found).toBeDefined();
228
+ expect(found!.version).toBe('abcdef1');
229
+ expect(found!.installedAt).toBe('2025-01-01T00:00:00.000Z');
230
+
231
+ delete lock['__test-list-plugin__'];
232
+ _writeLockFile(lock);
233
+ });
234
+
61
235
  it('returns empty array when no plugins dir', () => {
62
236
  // listPlugins should handle missing dir gracefully
63
237
  const plugins = listPlugins();
@@ -80,6 +254,22 @@ describe('uninstallPlugin', () => {
80
254
  expect(fs.existsSync(testDir)).toBe(false);
81
255
  });
82
256
 
257
+ it('removes lock entry on uninstall', () => {
258
+ fs.mkdirSync(testDir, { recursive: true });
259
+ fs.writeFileSync(path.join(testDir, 'test.yaml'), 'site: test');
260
+
261
+ const lock = _readLockFile();
262
+ lock['__test-uninstall__'] = {
263
+ source: 'https://github.com/user/repo.git',
264
+ commitHash: 'abc123',
265
+ installedAt: '2025-01-01T00:00:00.000Z',
266
+ };
267
+ _writeLockFile(lock);
268
+
269
+ uninstallPlugin('__test-uninstall__');
270
+ expect(_readLockFile()['__test-uninstall__']).toBeUndefined();
271
+ });
272
+
83
273
  it('throws for non-existent plugin', () => {
84
274
  expect(() => uninstallPlugin('__nonexistent__')).toThrow('not installed');
85
275
  });
@@ -90,3 +280,61 @@ describe('updatePlugin', () => {
90
280
  expect(() => updatePlugin('__nonexistent__')).toThrow('not installed');
91
281
  });
92
282
  });
283
+
284
+ vi.mock('node:child_process', () => {
285
+ return {
286
+ execFileSync: vi.fn((_cmd, args, opts) => {
287
+ if (Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
288
+ if (opts?.cwd === os.tmpdir()) {
289
+ throw new Error('not a git repository');
290
+ }
291
+ return '1234567890abcdef1234567890abcdef12345678\n';
292
+ }
293
+ if (opts && opts.cwd && String(opts.cwd).endsWith('plugin-b')) {
294
+ throw new Error('Network error');
295
+ }
296
+ return '';
297
+ }),
298
+ execSync: vi.fn(() => ''),
299
+ };
300
+ });
301
+
302
+ describe('updateAllPlugins', () => {
303
+ const testDirA = path.join(PLUGINS_DIR, 'plugin-a');
304
+ const testDirB = path.join(PLUGINS_DIR, 'plugin-b');
305
+ const testDirC = path.join(PLUGINS_DIR, 'plugin-c');
306
+
307
+ beforeEach(() => {
308
+ fs.mkdirSync(testDirA, { recursive: true });
309
+ fs.mkdirSync(testDirB, { recursive: true });
310
+ fs.mkdirSync(testDirC, { recursive: true });
311
+ fs.writeFileSync(path.join(testDirA, 'cmd.yaml'), 'site: a');
312
+ fs.writeFileSync(path.join(testDirB, 'cmd.yaml'), 'site: b');
313
+ fs.writeFileSync(path.join(testDirC, 'cmd.yaml'), 'site: c');
314
+ });
315
+
316
+ afterEach(() => {
317
+ try { fs.rmSync(testDirA, { recursive: true }); } catch {}
318
+ try { fs.rmSync(testDirB, { recursive: true }); } catch {}
319
+ try { fs.rmSync(testDirC, { recursive: true }); } catch {}
320
+ vi.clearAllMocks();
321
+ });
322
+
323
+ it('collects successes and failures without throwing', () => {
324
+ const results = _updateAllPlugins();
325
+
326
+ const resA = results.find(r => r.name === 'plugin-a');
327
+ const resB = results.find(r => r.name === 'plugin-b');
328
+ const resC = results.find(r => r.name === 'plugin-c');
329
+
330
+ expect(resA).toBeDefined();
331
+ expect(resA!.success).toBe(true);
332
+
333
+ expect(resB).toBeDefined();
334
+ expect(resB!.success).toBe(false);
335
+ expect(resB!.error).toContain('Network error');
336
+
337
+ expect(resC).toBeDefined();
338
+ expect(resC!.success).toBe(true);
339
+ });
340
+ });