@jackwener/opencli 1.6.1 → 1.6.2

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 (384) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +27 -45
  3. package/README.zh-CN.md +32 -34
  4. package/autoresearch/browse-tasks.json +18 -20
  5. package/autoresearch/commands/debug.ts +163 -0
  6. package/autoresearch/commands/fix.ts +145 -0
  7. package/autoresearch/commands/plan.ts +88 -0
  8. package/autoresearch/commands/run.ts +138 -0
  9. package/autoresearch/config.ts +82 -0
  10. package/autoresearch/engine.ts +359 -0
  11. package/autoresearch/eval-all.ts +127 -0
  12. package/autoresearch/eval-browse.ts +1 -1
  13. package/autoresearch/eval-publish.ts +238 -0
  14. package/autoresearch/eval-save.ts +249 -0
  15. package/autoresearch/eval-skill.ts +14 -8
  16. package/autoresearch/eval-v2ex.ts +220 -0
  17. package/autoresearch/eval-zhihu.ts +230 -0
  18. package/autoresearch/logger.ts +69 -0
  19. package/autoresearch/presets/combined-reliability.ts +27 -0
  20. package/autoresearch/presets/index.ts +23 -0
  21. package/autoresearch/presets/operate-reliability.ts +24 -0
  22. package/autoresearch/presets/save-reliability.ts +26 -0
  23. package/autoresearch/presets/skill-quality.ts +20 -0
  24. package/autoresearch/presets/v2ex-reliability.ts +24 -0
  25. package/autoresearch/presets/zhihu-reliability.ts +25 -0
  26. package/autoresearch/publish-tasks.json +345 -0
  27. package/autoresearch/run-save.sh +11 -0
  28. package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
  29. package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
  30. package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
  31. package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
  32. package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
  33. package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
  34. package/autoresearch/save-tasks.json +281 -0
  35. package/autoresearch/v2ex-tasks.json +899 -0
  36. package/autoresearch/zhihu-tasks.json +848 -0
  37. package/dist/browser/base-page.d.ts +4 -2
  38. package/dist/browser/base-page.js +37 -4
  39. package/dist/browser/bridge.js +10 -8
  40. package/dist/browser/cdp.js +2 -6
  41. package/dist/browser/daemon-client.d.ts +11 -1
  42. package/dist/browser/daemon-client.js +3 -0
  43. package/dist/browser/dom-helpers.d.ts +4 -2
  44. package/dist/browser/dom-helpers.js +42 -31
  45. package/dist/browser/dom-snapshot.js +23 -1
  46. package/dist/browser/page.d.ts +7 -2
  47. package/dist/browser/page.js +112 -30
  48. package/dist/browser.test.js +1 -1
  49. package/dist/build-manifest.d.ts +1 -0
  50. package/dist/build-manifest.js +1 -0
  51. package/dist/cli-manifest.json +1135 -184
  52. package/dist/cli.d.ts +2 -0
  53. package/dist/cli.js +48 -7
  54. package/dist/cli.test.d.ts +1 -0
  55. package/dist/cli.test.js +88 -0
  56. package/dist/clis/1688/item.d.ts +70 -0
  57. package/dist/clis/1688/item.js +187 -0
  58. package/dist/clis/1688/item.test.d.ts +1 -0
  59. package/dist/clis/1688/item.test.js +67 -0
  60. package/dist/clis/1688/search.d.ts +56 -0
  61. package/dist/clis/1688/search.js +309 -0
  62. package/dist/clis/1688/search.test.d.ts +1 -0
  63. package/dist/clis/1688/search.test.js +75 -0
  64. package/dist/clis/1688/shared.d.ts +112 -0
  65. package/dist/clis/1688/shared.js +514 -0
  66. package/dist/clis/1688/shared.test.d.ts +1 -0
  67. package/dist/clis/1688/shared.test.js +57 -0
  68. package/dist/clis/1688/store.d.ts +45 -0
  69. package/dist/clis/1688/store.js +226 -0
  70. package/dist/clis/1688/store.test.d.ts +1 -0
  71. package/dist/clis/1688/store.test.js +62 -0
  72. package/dist/clis/amazon/bestsellers.d.ts +0 -20
  73. package/dist/clis/amazon/bestsellers.js +6 -129
  74. package/dist/clis/amazon/bestsellers.test.js +12 -3
  75. package/dist/clis/amazon/movers-shakers.d.ts +1 -0
  76. package/dist/clis/amazon/movers-shakers.js +7 -0
  77. package/dist/clis/amazon/new-releases.d.ts +1 -0
  78. package/dist/clis/amazon/new-releases.js +7 -0
  79. package/dist/clis/amazon/rankings.d.ts +59 -0
  80. package/dist/clis/amazon/rankings.js +226 -0
  81. package/dist/clis/amazon/rankings.test.d.ts +1 -0
  82. package/dist/clis/amazon/rankings.test.js +41 -0
  83. package/dist/clis/amazon/shared.d.ts +11 -0
  84. package/dist/clis/amazon/shared.js +121 -11
  85. package/dist/clis/amazon/shared.test.js +11 -0
  86. package/dist/clis/bilibili/comments.js +2 -2
  87. package/dist/clis/bilibili/comments.test.js +3 -2
  88. package/dist/clis/bilibili/download.js +2 -1
  89. package/dist/clis/bilibili/subtitle.js +4 -3
  90. package/dist/clis/bilibili/subtitle.test.js +2 -1
  91. package/dist/clis/bilibili/utils.d.ts +5 -0
  92. package/dist/clis/bilibili/utils.js +30 -0
  93. package/dist/clis/bilibili/utils.test.d.ts +1 -0
  94. package/dist/clis/bilibili/utils.test.js +17 -0
  95. package/dist/clis/douban/marks.js +1 -1
  96. package/dist/clis/douban/subject.yaml +50 -19
  97. package/dist/clis/doubao/utils.js +32 -12
  98. package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
  99. package/dist/clis/douyin/_shared/transcode.test.js +0 -2
  100. package/dist/clis/douyin/draft.test.js +0 -2
  101. package/dist/clis/facebook/search.test.js +0 -2
  102. package/dist/clis/gemini/ask.js +9 -3
  103. package/dist/clis/gemini/ask.test.d.ts +1 -0
  104. package/dist/clis/gemini/ask.test.js +100 -0
  105. package/dist/clis/gemini/reply-state.test.d.ts +1 -0
  106. package/dist/clis/gemini/reply-state.test.js +641 -0
  107. package/dist/clis/gemini/utils.d.ts +44 -1
  108. package/dist/clis/gemini/utils.js +528 -61
  109. package/dist/clis/gemini/utils.test.js +149 -2
  110. package/dist/clis/hupu/detail.d.ts +1 -0
  111. package/dist/clis/hupu/detail.js +72 -0
  112. package/dist/clis/hupu/hot.yaml +43 -0
  113. package/dist/clis/hupu/like.d.ts +1 -0
  114. package/dist/clis/hupu/like.js +75 -0
  115. package/dist/clis/hupu/reply.d.ts +1 -0
  116. package/dist/clis/hupu/reply.js +71 -0
  117. package/dist/clis/hupu/search.d.ts +1 -0
  118. package/dist/clis/hupu/search.js +59 -0
  119. package/dist/clis/hupu/unlike.d.ts +1 -0
  120. package/dist/clis/hupu/unlike.js +75 -0
  121. package/dist/clis/hupu/utils.d.ts +20 -0
  122. package/dist/clis/hupu/utils.js +319 -0
  123. package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
  124. package/dist/clis/instagram/_shared/private-publish.js +1030 -0
  125. package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
  126. package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
  127. package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
  128. package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
  129. package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
  130. package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
  131. package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
  132. package/dist/clis/instagram/_shared/runtime-info.js +81 -0
  133. package/dist/clis/instagram/note.d.ts +1 -0
  134. package/dist/clis/instagram/note.js +222 -0
  135. package/dist/clis/instagram/note.test.d.ts +1 -0
  136. package/dist/clis/instagram/note.test.js +81 -0
  137. package/dist/clis/instagram/post.d.ts +4 -0
  138. package/dist/clis/instagram/post.js +1496 -0
  139. package/dist/clis/instagram/post.test.d.ts +1 -0
  140. package/dist/clis/instagram/post.test.js +1647 -0
  141. package/dist/clis/instagram/reel.d.ts +1 -0
  142. package/dist/clis/instagram/reel.js +826 -0
  143. package/dist/clis/instagram/reel.test.d.ts +1 -0
  144. package/dist/clis/instagram/reel.test.js +167 -0
  145. package/dist/clis/instagram/story.d.ts +1 -0
  146. package/dist/clis/instagram/story.js +115 -0
  147. package/dist/clis/instagram/story.test.d.ts +1 -0
  148. package/dist/clis/instagram/story.test.js +167 -0
  149. package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
  150. package/dist/clis/sinafinance/stock-rank.js +65 -0
  151. package/dist/clis/substack/utils.test.js +0 -2
  152. package/dist/clis/twitter/post.js +72 -45
  153. package/dist/clis/twitter/post.test.d.ts +1 -0
  154. package/dist/clis/twitter/post.test.js +116 -0
  155. package/dist/clis/twitter/reply.d.ts +12 -0
  156. package/dist/clis/twitter/reply.js +257 -35
  157. package/dist/clis/twitter/reply.test.d.ts +1 -0
  158. package/dist/clis/twitter/reply.test.js +151 -0
  159. package/dist/clis/xianyu/chat.d.ts +7 -0
  160. package/dist/clis/xianyu/chat.js +146 -0
  161. package/dist/clis/xianyu/chat.test.d.ts +1 -0
  162. package/dist/clis/xianyu/chat.test.js +15 -0
  163. package/dist/clis/xianyu/item.d.ts +7 -0
  164. package/dist/clis/xianyu/item.js +152 -0
  165. package/dist/clis/xianyu/item.test.d.ts +1 -0
  166. package/dist/clis/xianyu/item.test.js +56 -0
  167. package/dist/clis/xianyu/search.d.ts +10 -0
  168. package/dist/clis/xianyu/search.js +134 -0
  169. package/dist/clis/xianyu/search.test.d.ts +1 -0
  170. package/dist/clis/xianyu/search.test.js +17 -0
  171. package/dist/clis/xianyu/utils.d.ts +1 -0
  172. package/dist/clis/xianyu/utils.js +8 -0
  173. package/dist/clis/xiaoe/catalog.yaml +129 -0
  174. package/dist/clis/xiaoe/content.yaml +43 -0
  175. package/dist/clis/xiaoe/courses.yaml +73 -0
  176. package/dist/clis/xiaoe/detail.yaml +39 -0
  177. package/dist/clis/xiaoe/play-url.yaml +124 -0
  178. package/dist/clis/xiaohongshu/comments.test.js +0 -2
  179. package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
  180. package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
  181. package/dist/clis/xiaohongshu/download.test.js +0 -2
  182. package/dist/clis/xiaohongshu/note.test.js +0 -2
  183. package/dist/clis/xiaohongshu/publish.test.js +0 -2
  184. package/dist/clis/xiaohongshu/search.js +29 -20
  185. package/dist/clis/xiaohongshu/search.test.js +56 -48
  186. package/dist/clis/yuanbao/ask.d.ts +21 -0
  187. package/dist/clis/yuanbao/ask.js +427 -0
  188. package/dist/clis/yuanbao/ask.test.d.ts +1 -0
  189. package/dist/clis/yuanbao/ask.test.js +124 -0
  190. package/dist/clis/yuanbao/new.d.ts +1 -0
  191. package/dist/clis/yuanbao/new.js +70 -0
  192. package/dist/clis/yuanbao/new.test.d.ts +1 -0
  193. package/dist/clis/yuanbao/new.test.js +30 -0
  194. package/dist/clis/yuanbao/shared.d.ts +13 -0
  195. package/dist/clis/yuanbao/shared.js +49 -0
  196. package/dist/clis/zhihu/question.js +30 -19
  197. package/dist/clis/zhihu/question.test.js +34 -16
  198. package/dist/commanderAdapter.js +8 -4
  199. package/dist/commanderAdapter.test.js +42 -0
  200. package/dist/completion.js +3 -1
  201. package/dist/completion.test.d.ts +1 -0
  202. package/dist/completion.test.js +23 -0
  203. package/dist/doctor.js +1 -1
  204. package/dist/electron-apps.d.ts +2 -0
  205. package/dist/electron-apps.js +7 -1
  206. package/dist/errors.js +1 -1
  207. package/dist/execution.js +25 -35
  208. package/dist/explore.js +1 -1
  209. package/dist/launcher.d.ts +4 -0
  210. package/dist/launcher.js +64 -8
  211. package/dist/launcher.test.js +88 -7
  212. package/dist/output.d.ts +2 -0
  213. package/dist/output.js +10 -1
  214. package/dist/output.test.d.ts +0 -3
  215. package/dist/output.test.js +59 -92
  216. package/dist/pipeline/executor.test.js +0 -2
  217. package/dist/pipeline/steps/download.test.js +0 -2
  218. package/dist/registry.d.ts +2 -0
  219. package/dist/serialization.d.ts +1 -0
  220. package/dist/serialization.js +1 -0
  221. package/dist/types.d.ts +9 -2
  222. package/docs/.vitepress/config.mts +4 -0
  223. package/docs/adapters/browser/1688.md +52 -0
  224. package/docs/adapters/browser/36kr.md +2 -1
  225. package/docs/adapters/browser/doubao.md +5 -1
  226. package/docs/adapters/browser/hupu.md +53 -0
  227. package/docs/adapters/browser/sinafinance.md +32 -2
  228. package/docs/adapters/browser/weibo.md +6 -1
  229. package/docs/adapters/browser/wikipedia.md +2 -0
  230. package/docs/adapters/browser/xianyu.md +42 -0
  231. package/docs/adapters/browser/xiaoe.md +44 -0
  232. package/docs/adapters/browser/yuanbao.md +64 -0
  233. package/docs/adapters/index.md +14 -5
  234. package/docs/comparison.md +1 -1
  235. package/docs/developer/ai-workflow.md +2 -2
  236. package/docs/developer/contributing.md +1 -1
  237. package/docs/developer/testing.md +2 -0
  238. package/docs/guide/plugins.md +1 -0
  239. package/docs/guide/troubleshooting.md +11 -0
  240. package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
  241. package/docs/zh/guide/plugins.md +1 -0
  242. package/extension/dist/background.js +1127 -0
  243. package/extension/src/background.test.ts +39 -0
  244. package/extension/src/background.ts +223 -34
  245. package/extension/src/cdp.ts +194 -4
  246. package/extension/src/protocol.ts +22 -1
  247. package/package.json +3 -2
  248. package/scripts/postinstall.js +1 -1
  249. package/skills/opencli-explorer/SKILL.md +1 -1
  250. package/skills/opencli-oneshot/SKILL.md +2 -2
  251. package/skills/opencli-operate/SKILL.md +120 -27
  252. package/skills/opencli-usage/SKILL.md +31 -20
  253. package/skills/opencli-usage/browser.md +114 -16
  254. package/skills/opencli-usage/public-api.md +32 -3
  255. package/skills/smart-search/SKILL.md +156 -0
  256. package/skills/smart-search/references/sources-ai.md +74 -0
  257. package/skills/smart-search/references/sources-info.md +43 -0
  258. package/skills/smart-search/references/sources-media.md +50 -0
  259. package/skills/smart-search/references/sources-other.md +42 -0
  260. package/skills/smart-search/references/sources-shopping.md +31 -0
  261. package/skills/smart-search/references/sources-social.md +51 -0
  262. package/skills/smart-search/references/sources-tech.md +42 -0
  263. package/skills/smart-search/references/sources-travel.md +20 -0
  264. package/src/browser/base-page.ts +41 -6
  265. package/src/browser/bridge.ts +11 -8
  266. package/src/browser/cdp.ts +1 -8
  267. package/src/browser/daemon-client.ts +11 -1
  268. package/src/browser/dom-helpers.ts +43 -31
  269. package/src/browser/dom-snapshot.ts +23 -1
  270. package/src/browser/page.ts +115 -31
  271. package/src/browser.test.ts +1 -1
  272. package/src/build-manifest.ts +2 -0
  273. package/src/cli.test.ts +133 -0
  274. package/src/cli.ts +73 -11
  275. package/src/clis/1688/item.test.ts +69 -0
  276. package/src/clis/1688/item.ts +282 -0
  277. package/src/clis/1688/search.test.ts +81 -0
  278. package/src/clis/1688/search.ts +402 -0
  279. package/src/clis/1688/shared.test.ts +75 -0
  280. package/src/clis/1688/shared.ts +623 -0
  281. package/src/clis/1688/store.test.ts +69 -0
  282. package/src/clis/1688/store.ts +300 -0
  283. package/src/clis/amazon/bestsellers.test.ts +12 -3
  284. package/src/clis/amazon/bestsellers.ts +6 -178
  285. package/src/clis/amazon/movers-shakers.ts +8 -0
  286. package/src/clis/amazon/new-releases.ts +8 -0
  287. package/src/clis/amazon/rankings.test.ts +47 -0
  288. package/src/clis/amazon/rankings.ts +312 -0
  289. package/src/clis/amazon/shared.test.ts +16 -0
  290. package/src/clis/amazon/shared.ts +134 -12
  291. package/src/clis/bilibili/comments.test.ts +4 -3
  292. package/src/clis/bilibili/comments.ts +2 -2
  293. package/src/clis/bilibili/download.ts +2 -1
  294. package/src/clis/bilibili/subtitle.test.ts +2 -1
  295. package/src/clis/bilibili/subtitle.ts +4 -3
  296. package/src/clis/bilibili/utils.test.ts +21 -0
  297. package/src/clis/bilibili/utils.ts +27 -0
  298. package/src/clis/douban/marks.ts +1 -1
  299. package/src/clis/douban/subject.yaml +50 -19
  300. package/src/clis/doubao/utils.ts +32 -12
  301. package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
  302. package/src/clis/douyin/_shared/transcode.test.ts +0 -2
  303. package/src/clis/douyin/draft.test.ts +0 -2
  304. package/src/clis/facebook/search.test.ts +0 -2
  305. package/src/clis/gemini/ask.test.ts +116 -0
  306. package/src/clis/gemini/ask.ts +10 -3
  307. package/src/clis/gemini/reply-state.test.ts +708 -0
  308. package/src/clis/gemini/utils.test.ts +184 -2
  309. package/src/clis/gemini/utils.ts +588 -60
  310. package/src/clis/hupu/detail.ts +126 -0
  311. package/src/clis/hupu/hot.yaml +43 -0
  312. package/src/clis/hupu/like.ts +76 -0
  313. package/src/clis/hupu/reply.ts +76 -0
  314. package/src/clis/hupu/search.ts +95 -0
  315. package/src/clis/hupu/unlike.ts +76 -0
  316. package/src/clis/hupu/utils.ts +381 -0
  317. package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
  318. package/src/clis/instagram/_shared/private-publish.ts +1303 -0
  319. package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
  320. package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
  321. package/src/clis/instagram/_shared/runtime-info.ts +91 -0
  322. package/src/clis/instagram/note.test.ts +96 -0
  323. package/src/clis/instagram/note.ts +254 -0
  324. package/src/clis/instagram/post.test.ts +1716 -0
  325. package/src/clis/instagram/post.ts +1620 -0
  326. package/src/clis/instagram/reel.test.ts +191 -0
  327. package/src/clis/instagram/reel.ts +886 -0
  328. package/src/clis/instagram/story.test.ts +191 -0
  329. package/src/clis/instagram/story.ts +151 -0
  330. package/src/clis/sinafinance/stock-rank.ts +68 -0
  331. package/src/clis/substack/utils.test.ts +0 -2
  332. package/src/clis/twitter/post.test.ts +157 -0
  333. package/src/clis/twitter/post.ts +82 -48
  334. package/src/clis/twitter/reply.test.ts +177 -0
  335. package/src/clis/twitter/reply.ts +285 -39
  336. package/src/clis/xianyu/chat.test.ts +20 -0
  337. package/src/clis/xianyu/chat.ts +175 -0
  338. package/src/clis/xianyu/item.test.ts +67 -0
  339. package/src/clis/xianyu/item.ts +172 -0
  340. package/src/clis/xianyu/search.test.ts +22 -0
  341. package/src/clis/xianyu/search.ts +151 -0
  342. package/src/clis/xianyu/utils.ts +9 -0
  343. package/src/clis/xiaoe/catalog.yaml +129 -0
  344. package/src/clis/xiaoe/content.yaml +43 -0
  345. package/src/clis/xiaoe/courses.yaml +73 -0
  346. package/src/clis/xiaoe/detail.yaml +39 -0
  347. package/src/clis/xiaoe/play-url.yaml +124 -0
  348. package/src/clis/xiaohongshu/comments.test.ts +0 -2
  349. package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
  350. package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
  351. package/src/clis/xiaohongshu/download.test.ts +0 -2
  352. package/src/clis/xiaohongshu/note.test.ts +0 -2
  353. package/src/clis/xiaohongshu/publish.test.ts +0 -2
  354. package/src/clis/xiaohongshu/search.test.ts +59 -48
  355. package/src/clis/xiaohongshu/search.ts +31 -21
  356. package/src/clis/yuanbao/ask.test.ts +156 -0
  357. package/src/clis/yuanbao/ask.ts +522 -0
  358. package/src/clis/yuanbao/new.test.ts +36 -0
  359. package/src/clis/yuanbao/new.ts +81 -0
  360. package/src/clis/yuanbao/shared.ts +57 -0
  361. package/src/clis/zhihu/question.test.ts +42 -17
  362. package/src/clis/zhihu/question.ts +31 -26
  363. package/src/commanderAdapter.test.ts +51 -0
  364. package/src/commanderAdapter.ts +8 -4
  365. package/src/completion.test.ts +30 -0
  366. package/src/completion.ts +3 -1
  367. package/src/doctor.ts +1 -1
  368. package/src/electron-apps.ts +9 -1
  369. package/src/errors.ts +1 -1
  370. package/src/execution.ts +26 -30
  371. package/src/explore.ts +1 -1
  372. package/src/launcher.test.ts +121 -7
  373. package/src/launcher.ts +87 -9
  374. package/src/output.test.ts +50 -90
  375. package/src/output.ts +10 -1
  376. package/src/pipeline/executor.test.ts +0 -2
  377. package/src/pipeline/steps/download.test.ts +0 -2
  378. package/src/registry.ts +2 -0
  379. package/src/serialization.ts +2 -0
  380. package/src/types.ts +9 -2
  381. package/tests/e2e/browser-auth.test.ts +9 -0
  382. package/CLI-EXPLORER.md +0 -724
  383. package/CLI-ONESHOT.md +0 -216
  384. package/SKILL.md +0 -59
@@ -0,0 +1,191 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+
5
+ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import { ArgumentError } from '../../errors.js';
8
+ import { getRegistry } from '../../registry.js';
9
+ import type { IPage } from '../../types.js';
10
+ import * as privatePublish from './_shared/private-publish.js';
11
+ import './story.js';
12
+
13
+ const tempDirs: string[] = [];
14
+
15
+ function createTempFile(name: string, bytes = Buffer.from('story-media')): string {
16
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-instagram-story-'));
17
+ tempDirs.push(dir);
18
+ const filePath = path.join(dir, name);
19
+ fs.writeFileSync(filePath, bytes);
20
+ return filePath;
21
+ }
22
+
23
+ function createPageMock(evaluateResults: unknown[] = [], overrides: Partial<IPage> = {}): IPage {
24
+ const evaluate = vi.fn();
25
+ for (const result of evaluateResults) {
26
+ evaluate.mockResolvedValueOnce(result);
27
+ }
28
+ return {
29
+ goto: vi.fn().mockResolvedValue(undefined),
30
+ evaluate,
31
+ getCookies: vi.fn().mockResolvedValue([]),
32
+ snapshot: vi.fn().mockResolvedValue(undefined),
33
+ click: vi.fn().mockResolvedValue(undefined),
34
+ typeText: vi.fn().mockResolvedValue(undefined),
35
+ pressKey: vi.fn().mockResolvedValue(undefined),
36
+ scrollTo: vi.fn().mockResolvedValue(undefined),
37
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
38
+ wait: vi.fn().mockResolvedValue(undefined),
39
+ tabs: vi.fn().mockResolvedValue([]),
40
+ closeTab: vi.fn().mockResolvedValue(undefined),
41
+ newTab: vi.fn().mockResolvedValue(undefined),
42
+ selectTab: vi.fn().mockResolvedValue(undefined),
43
+ networkRequests: vi.fn().mockResolvedValue([]),
44
+ consoleMessages: vi.fn().mockResolvedValue([]),
45
+ scroll: vi.fn().mockResolvedValue(undefined),
46
+ autoScroll: vi.fn().mockResolvedValue(undefined),
47
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
48
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
49
+ waitForCapture: vi.fn().mockResolvedValue(undefined),
50
+ screenshot: vi.fn().mockResolvedValue(''),
51
+ setFileInput: vi.fn().mockResolvedValue(undefined),
52
+ insertText: vi.fn().mockResolvedValue(undefined),
53
+ getCurrentUrl: vi.fn().mockResolvedValue(null),
54
+ ...overrides,
55
+ };
56
+ }
57
+
58
+ afterAll(() => {
59
+ for (const dir of tempDirs) {
60
+ fs.rmSync(dir, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ describe('instagram story registration', () => {
65
+ beforeEach(() => {
66
+ vi.restoreAllMocks();
67
+ });
68
+
69
+ afterEach(() => {
70
+ vi.restoreAllMocks();
71
+ });
72
+
73
+ it('registers the story command with a required-value media arg', () => {
74
+ const cmd = getRegistry().get('instagram/story');
75
+ expect(cmd).toBeDefined();
76
+ expect(cmd?.browser).toBe(true);
77
+ expect(cmd?.args.some((arg) => arg.name === 'media' && !arg.required && arg.valueRequired)).toBe(true);
78
+ expect(cmd?.args.some((arg) => arg.name === 'content')).toBe(false);
79
+ });
80
+
81
+ it('rejects missing --media before browser work', async () => {
82
+ const page = createPageMock();
83
+ const cmd = getRegistry().get('instagram/story');
84
+
85
+ await expect(cmd!.func!(page, {})).rejects.toThrow(ArgumentError);
86
+ expect(page.goto).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('rejects multiple media inputs for a single story', async () => {
90
+ const first = createTempFile('one.jpg');
91
+ const second = createTempFile('two.mp4');
92
+ const page = createPageMock();
93
+ const cmd = getRegistry().get('instagram/story');
94
+
95
+ await expect(cmd!.func!(page, { media: `${first},${second}` })).rejects.toThrow('single media');
96
+ expect(page.goto).not.toHaveBeenCalled();
97
+ });
98
+
99
+ it('rejects unsupported story formats', async () => {
100
+ const filePath = createTempFile('story.mov');
101
+ const page = createPageMock();
102
+ const cmd = getRegistry().get('instagram/story');
103
+
104
+ await expect(cmd!.func!(page, { media: filePath })).rejects.toThrow('Unsupported story media format');
105
+ expect(page.goto).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('publishes a single image story through the private route', async () => {
109
+ const imagePath = createTempFile('story.jpg');
110
+ const page = createPageMock([
111
+ { appId: '936619743392459', csrfToken: '', instagramAjax: 'ajax' },
112
+ { ok: true, username: 'tsezi_ray' },
113
+ ], {
114
+ getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '123', domain: 'instagram.com' }]),
115
+ });
116
+ const cmd = getRegistry().get('instagram/story');
117
+
118
+ vi.spyOn(privatePublish, 'resolveInstagramPrivatePublishConfig').mockResolvedValue({
119
+ apiContext: {
120
+ asbdId: '359341',
121
+ csrfToken: 'csrf-token',
122
+ igAppId: '936619743392459',
123
+ igWwwClaim: 'claim',
124
+ instagramAjax: 'ajax',
125
+ webSessionId: 'session',
126
+ },
127
+ jazoest: '22047',
128
+ });
129
+ vi.spyOn(privatePublish, 'publishStoryViaPrivateApi').mockResolvedValue({
130
+ mediaPk: '1234567890',
131
+ uploadId: '1234567890',
132
+ });
133
+
134
+ const result = await cmd!.func!(page, { media: imagePath });
135
+
136
+ expect(privatePublish.publishStoryViaPrivateApi).toHaveBeenCalledWith(expect.objectContaining({
137
+ page,
138
+ mediaItem: { type: 'image', filePath: imagePath },
139
+ content: '',
140
+ }));
141
+ expect(result).toEqual([
142
+ {
143
+ status: '✅ Posted',
144
+ detail: 'Single story shared successfully',
145
+ url: 'https://www.instagram.com/stories/tsezi_ray/1234567890/',
146
+ },
147
+ ]);
148
+ });
149
+
150
+ it('publishes a single video story through the private route', async () => {
151
+ const videoPath = createTempFile('story.mp4');
152
+ const page = createPageMock([
153
+ { appId: '936619743392459', csrfToken: '', instagramAjax: 'ajax' },
154
+ { ok: true, username: 'tsezi_ray' },
155
+ ], {
156
+ getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '123', domain: 'instagram.com' }]),
157
+ });
158
+ const cmd = getRegistry().get('instagram/story');
159
+
160
+ vi.spyOn(privatePublish, 'resolveInstagramPrivatePublishConfig').mockResolvedValue({
161
+ apiContext: {
162
+ asbdId: '359341',
163
+ csrfToken: 'csrf-token',
164
+ igAppId: '936619743392459',
165
+ igWwwClaim: 'claim',
166
+ instagramAjax: 'ajax',
167
+ webSessionId: 'session',
168
+ },
169
+ jazoest: '22047',
170
+ });
171
+ vi.spyOn(privatePublish, 'publishStoryViaPrivateApi').mockResolvedValue({
172
+ mediaPk: '9988776655',
173
+ uploadId: '9988776655',
174
+ });
175
+
176
+ const result = await cmd!.func!(page, { media: videoPath });
177
+
178
+ expect(privatePublish.publishStoryViaPrivateApi).toHaveBeenCalledWith(expect.objectContaining({
179
+ page,
180
+ mediaItem: { type: 'video', filePath: videoPath },
181
+ content: '',
182
+ }));
183
+ expect(result).toEqual([
184
+ {
185
+ status: '✅ Posted',
186
+ detail: 'Single video story shared successfully',
187
+ url: 'https://www.instagram.com/stories/tsezi_ray/9988776655/',
188
+ },
189
+ ]);
190
+ });
191
+ });
@@ -0,0 +1,151 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ import { ArgumentError, CommandExecutionError } from '../../errors.js';
5
+ import { cli, Strategy } from '../../registry.js';
6
+ import type { IPage } from '../../types.js';
7
+ import {
8
+ publishStoryViaPrivateApi,
9
+ resolveInstagramPrivatePublishConfig,
10
+ } from './_shared/private-publish.js';
11
+ import { resolveInstagramRuntimeInfo } from './_shared/runtime-info.js';
12
+
13
+ const INSTAGRAM_HOME_URL = 'https://www.instagram.com/';
14
+ const SUPPORTED_STORY_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
15
+ const SUPPORTED_STORY_VIDEO_EXTENSIONS = new Set(['.mp4']);
16
+
17
+ type InstagramStoryMediaItem = {
18
+ type: 'image' | 'video';
19
+ filePath: string;
20
+ };
21
+
22
+ type InstagramStorySuccessRow = {
23
+ status: string;
24
+ detail: string;
25
+ url: string;
26
+ };
27
+
28
+ function requirePage(page: IPage | null): IPage {
29
+ if (!page) throw new CommandExecutionError('Browser session required for instagram story');
30
+ return page;
31
+ }
32
+
33
+ function validateInstagramStoryArgs(kwargs: Record<string, unknown>): void {
34
+ if (kwargs.media === undefined) {
35
+ throw new ArgumentError(
36
+ 'Argument "media" is required.',
37
+ 'Provide --media /path/to/file.jpg or --media /path/to/file.mp4',
38
+ );
39
+ }
40
+ }
41
+
42
+ function normalizeStoryMediaItem(kwargs: Record<string, unknown>): InstagramStoryMediaItem {
43
+ const raw = String(kwargs.media ?? '').trim();
44
+ const parts = raw.split(',').map((part) => part.trim()).filter(Boolean);
45
+ if (parts.length === 0) {
46
+ throw new ArgumentError(
47
+ 'Argument "media" is required.',
48
+ 'Provide --media /path/to/file.jpg or --media /path/to/file.mp4',
49
+ );
50
+ }
51
+ if (parts.length > 1) {
52
+ throw new ArgumentError(
53
+ 'Instagram story currently supports a single media item.',
54
+ 'Provide one image or one video path with --media',
55
+ );
56
+ }
57
+
58
+ const resolved = path.resolve(parts[0]!);
59
+ if (!fs.existsSync(resolved)) {
60
+ throw new ArgumentError(`Story media file not found: ${resolved}`);
61
+ }
62
+ const ext = path.extname(resolved).toLowerCase();
63
+ if (SUPPORTED_STORY_IMAGE_EXTENSIONS.has(ext)) {
64
+ return { type: 'image', filePath: resolved };
65
+ }
66
+ if (SUPPORTED_STORY_VIDEO_EXTENSIONS.has(ext)) {
67
+ return { type: 'video', filePath: resolved };
68
+ }
69
+ throw new ArgumentError(
70
+ `Unsupported story media format: ${ext}`,
71
+ 'Supported formats: images (.jpg, .jpeg, .png, .webp) and videos (.mp4)',
72
+ );
73
+ }
74
+
75
+ async function resolveCurrentUserId(page: IPage): Promise<string> {
76
+ const cookies = await page.getCookies({ domain: 'instagram.com' });
77
+ return cookies.find((cookie) => cookie.name === 'ds_user_id')?.value || '';
78
+ }
79
+
80
+ async function resolveCurrentUsername(page: IPage, currentUserId = ''): Promise<string> {
81
+ if (!currentUserId) return '';
82
+ const runtimeInfo = await resolveInstagramRuntimeInfo(page);
83
+ const apiResult = await page.evaluate(`
84
+ (async () => {
85
+ const userId = ${JSON.stringify(currentUserId)};
86
+ const appId = ${JSON.stringify(runtimeInfo.appId || '')};
87
+ try {
88
+ const res = await fetch(
89
+ 'https://www.instagram.com/api/v1/users/' + encodeURIComponent(userId) + '/info/',
90
+ {
91
+ credentials: 'include',
92
+ headers: appId ? { 'X-IG-App-ID': appId } : {},
93
+ },
94
+ );
95
+ if (!res.ok) return { ok: false };
96
+ const data = await res.json();
97
+ const username = data?.user?.username || '';
98
+ return { ok: !!username, username };
99
+ } catch {
100
+ return { ok: false };
101
+ }
102
+ })()
103
+ `) as { ok?: boolean; username?: string };
104
+
105
+ return apiResult?.ok && apiResult.username ? apiResult.username : '';
106
+ }
107
+
108
+ function buildStorySuccessResult(mediaItem: InstagramStoryMediaItem, url: string): InstagramStorySuccessRow[] {
109
+ return [{
110
+ status: '✅ Posted',
111
+ detail: mediaItem.type === 'video'
112
+ ? 'Single video story shared successfully'
113
+ : 'Single story shared successfully',
114
+ url,
115
+ }];
116
+ }
117
+
118
+ cli({
119
+ site: 'instagram',
120
+ name: 'story',
121
+ description: 'Post a single Instagram story image or video',
122
+ domain: 'www.instagram.com',
123
+ strategy: Strategy.UI,
124
+ browser: true,
125
+ timeoutSeconds: 300,
126
+ args: [
127
+ { name: 'media', required: false, valueRequired: true, help: 'Path to a single story image or video file' },
128
+ ],
129
+ columns: ['status', 'detail', 'url'],
130
+ validateArgs: validateInstagramStoryArgs,
131
+ func: async (page: IPage | null, kwargs) => {
132
+ const browserPage = requirePage(page);
133
+ const mediaItem = normalizeStoryMediaItem(kwargs as Record<string, unknown>);
134
+ const currentUserId = await resolveCurrentUserId(browserPage);
135
+ const privateConfig = await resolveInstagramPrivatePublishConfig(browserPage);
136
+ const storyResult = await publishStoryViaPrivateApi({
137
+ page: browserPage,
138
+ mediaItem,
139
+ content: '',
140
+ apiContext: privateConfig.apiContext,
141
+ jazoest: privateConfig.jazoest,
142
+ currentUserId,
143
+ });
144
+ const username = await resolveCurrentUsername(browserPage, currentUserId);
145
+ const mediaPk = storyResult.mediaPk || storyResult.uploadId;
146
+ const url = username && mediaPk
147
+ ? new URL(`/stories/${username}/${mediaPk}/`, INSTAGRAM_HOME_URL).toString()
148
+ : '';
149
+ return buildStorySuccessResult(mediaItem, url);
150
+ },
151
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Sinafinance stock rank
3
+ */
4
+
5
+ import { cli, Strategy } from '../../registry.js';
6
+
7
+ cli({
8
+ site: 'sinafinance',
9
+ name: 'stock-rank',
10
+ description: '新浪财经热搜榜',
11
+ domain: 'finance.sina.cn',
12
+ strategy: Strategy.COOKIE,
13
+ navigateBefore: false,
14
+ args: [
15
+ { name: 'market', type: 'string', default: 'cn', choices: ['cn', 'hk', 'us', 'wh', 'ft'], help: 'Market: cn (A股), hk (港股), us (美股), wh (外汇), ft (期货)' },
16
+ ],
17
+ columns: ['rank', 'name', 'symbol', 'market', 'price', 'change', 'url'],
18
+ func: async (page, _args) => {
19
+ const market = _args.market || 'cn';
20
+
21
+ await page.goto('https://finance.sina.cn/');
22
+ await page.wait({ selector: '#actionSearch', timeout: 10000 });
23
+
24
+ const payload = await page.evaluate(`
25
+ (async () => {
26
+ const wait = (ms) => new Promise(r => setTimeout(r, ms));
27
+ const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
28
+ const marketType = ${JSON.stringify(market)};
29
+
30
+ const searchBtn = document.querySelector('#actionSearch');
31
+ if (searchBtn) {
32
+ searchBtn.dispatchEvent(new Event('tap', { bubbles: true }));
33
+ await wait(3000);
34
+ }
35
+
36
+ const tabEl = document.querySelector('[data-type="' + marketType + '"]');
37
+ const marketName = tabEl?.textContent || marketType;
38
+ if (marketType !== 'cn' && tabEl) {
39
+ tabEl.click();
40
+ await wait(2000);
41
+ }
42
+
43
+ const results = [];
44
+ document.querySelectorAll('#stock-list .j-stock-row').forEach(el => {
45
+ const rankEl = el.querySelector('.rank');
46
+ const nameEl = el.querySelector('.j-sname');
47
+ const codeEl = el.querySelector('.stock-code');
48
+ const priceEl = el.querySelector('.j-price');
49
+ const changeEl = el.querySelector('.j-change');
50
+ const openUrl = el.getAttribute('open-url') || '';
51
+ const fullUrl = openUrl ? 'https:' + openUrl : '';
52
+ results.push({
53
+ rank: cleanText(rankEl?.textContent || ''),
54
+ name: cleanText(nameEl?.textContent || ''),
55
+ symbol: cleanText(codeEl?.textContent || ''),
56
+ market: cleanText(marketName),
57
+ price: cleanText(priceEl?.textContent || ''),
58
+ change: cleanText(changeEl?.textContent || ''),
59
+ url: fullUrl,
60
+ });
61
+ });
62
+ return results;
63
+ })()
64
+ `);
65
+ if (!Array.isArray(payload)) return [];
66
+ return payload;
67
+ },
68
+ });
@@ -14,8 +14,6 @@ function createPageMock(evaluateResult: unknown): IPage {
14
14
  getFormState: vi.fn().mockResolvedValue({}),
15
15
  wait: vi.fn().mockResolvedValue(undefined),
16
16
  tabs: vi.fn().mockResolvedValue([]),
17
- closeTab: vi.fn().mockResolvedValue(undefined),
18
- newTab: vi.fn().mockResolvedValue(undefined),
19
17
  selectTab: vi.fn().mockResolvedValue(undefined),
20
18
  networkRequests: vi.fn().mockResolvedValue([]),
21
19
  consoleMessages: vi.fn().mockResolvedValue([]),
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './post.js';
4
+
5
+ vi.mock('node:fs', async (importOriginal) => {
6
+ const actual = await importOriginal<typeof import('node:fs')>();
7
+ return {
8
+ ...actual,
9
+ statSync: vi.fn((p: string, _opts?: any) => {
10
+ if (String(p).includes('missing')) return undefined;
11
+ return { isFile: () => true };
12
+ }),
13
+ };
14
+ });
15
+
16
+ vi.mock('node:path', async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import('node:path')>();
18
+ return {
19
+ ...actual,
20
+ resolve: vi.fn((p: string) => `/abs/${p}`),
21
+ extname: vi.fn((p: string) => {
22
+ const m = p.match(/\.[^.]+$/);
23
+ return m ? m[0] : '';
24
+ }),
25
+ };
26
+ });
27
+
28
+ function makePage(overrides: Record<string, any> = {}) {
29
+ return {
30
+ goto: vi.fn().mockResolvedValue(undefined),
31
+ wait: vi.fn().mockResolvedValue(undefined),
32
+ evaluate: vi.fn().mockResolvedValue({ ok: true }),
33
+ setFileInput: vi.fn().mockResolvedValue(undefined),
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ describe('twitter post command', () => {
39
+ const getCommand = () => getRegistry().get('twitter/post');
40
+
41
+ it('posts text-only tweet successfully', async () => {
42
+ const command = getCommand();
43
+ const page = makePage({
44
+ evaluate: vi.fn()
45
+ .mockResolvedValueOnce({ ok: true })
46
+ .mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }),
47
+ });
48
+
49
+ const result = await command!.func!(page as any, { text: 'hello world' });
50
+
51
+ expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'hello world' }]);
52
+ expect(page.goto).toHaveBeenCalledWith('https://x.com/compose/tweet');
53
+ });
54
+
55
+ it('returns failed when text area not found', async () => {
56
+ const command = getCommand();
57
+ const page = makePage({
58
+ evaluate: vi.fn()
59
+ .mockResolvedValueOnce({ ok: false, message: 'Could not find the tweet composer text area.' }),
60
+ });
61
+
62
+ const result = await command!.func!(page as any, { text: 'hello' });
63
+
64
+ expect(result).toEqual([{ status: 'failed', message: 'Could not find the tweet composer text area.', text: 'hello' }]);
65
+ });
66
+
67
+ it('throws when more than 4 images', async () => {
68
+ const command = getCommand();
69
+ const page = makePage();
70
+
71
+ await expect(
72
+ command!.func!(page as any, { text: 'hi', images: 'a.png,b.png,c.png,d.png,e.png' }),
73
+ ).rejects.toThrow('Too many images: 5 (max 4)');
74
+ });
75
+
76
+ it('throws when image file does not exist', async () => {
77
+ const command = getCommand();
78
+ const page = makePage();
79
+
80
+ await expect(
81
+ command!.func!(page as any, { text: 'hi', images: 'missing.png' }),
82
+ ).rejects.toThrow('Not a valid file');
83
+ });
84
+
85
+ it('throws on unsupported image format', async () => {
86
+ const command = getCommand();
87
+ const page = makePage();
88
+
89
+ await expect(
90
+ command!.func!(page as any, { text: 'hi', images: 'photo.bmp' }),
91
+ ).rejects.toThrow('Unsupported image format');
92
+ });
93
+
94
+ it('throws when page.setFileInput is not available', async () => {
95
+ const command = getCommand();
96
+ const page = makePage({
97
+ evaluate: vi.fn().mockResolvedValueOnce({ ok: true }),
98
+ setFileInput: undefined,
99
+ });
100
+
101
+ await expect(
102
+ command!.func!(page as any, { text: 'hi', images: 'a.png' }),
103
+ ).rejects.toThrow('Browser extension does not support file upload');
104
+ });
105
+
106
+ it('posts with images when upload completes', async () => {
107
+ const command = getCommand();
108
+ const page = makePage({
109
+ evaluate: vi.fn()
110
+ .mockResolvedValueOnce({ ok: true }) // type text
111
+ .mockResolvedValueOnce(true) // upload polling returns true
112
+ .mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }), // click post
113
+ });
114
+
115
+ const result = await command!.func!(page as any, { text: 'with images', images: 'a.png,b.png' });
116
+
117
+ expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'with images' }]);
118
+ expect(page.setFileInput).toHaveBeenCalled();
119
+
120
+ const uploadScript = page.evaluate.mock.calls[1][0] as string;
121
+ expect(uploadScript).toContain('[data-testid="attachments"]');
122
+ expect(uploadScript).toContain('[role="group"]');
123
+ });
124
+
125
+ it('returns failed when image upload times out', async () => {
126
+ const command = getCommand();
127
+ const page = makePage({
128
+ evaluate: vi.fn()
129
+ .mockResolvedValueOnce({ ok: true })
130
+ .mockResolvedValueOnce(false),
131
+ });
132
+
133
+ const result = await command!.func!(page as any, { text: 'timeout', images: 'a.png' });
134
+
135
+ expect(result).toEqual([{ status: 'failed', message: 'Image upload timed out (30s).', text: 'timeout' }]);
136
+ });
137
+
138
+ it('validates images before navigating to compose page', async () => {
139
+ const command = getCommand();
140
+ const page = makePage();
141
+
142
+ await expect(
143
+ command!.func!(page as any, { text: 'hi', images: 'missing.png' }),
144
+ ).rejects.toThrow('Not a valid file');
145
+
146
+ // Should NOT have navigated since validation happens first
147
+ expect(page.goto).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it('throws when no browser session', async () => {
151
+ const command = getCommand();
152
+
153
+ await expect(
154
+ command!.func!(null as any, { text: 'hi' }),
155
+ ).rejects.toThrow('Browser session required for twitter post');
156
+ });
157
+ });