@jackwener/opencli 1.0.6 → 1.1.1

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 (400) hide show
  1. package/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
  2. package/.github/pull_request_template.md +7 -0
  3. package/.github/workflows/doc-check.yml +36 -0
  4. package/.github/workflows/docs.yml +7 -42
  5. package/CHANGELOG.md +23 -0
  6. package/CLI-EXPLORER.md +9 -8
  7. package/README.md +51 -10
  8. package/README.zh-CN.md +29 -11
  9. package/SKILL.md +102 -33
  10. package/dist/browser/cdp.js +6 -1
  11. package/dist/browser/page.d.ts +4 -1
  12. package/dist/browser/page.js +7 -1
  13. package/dist/build-manifest.js +23 -16
  14. package/dist/cli-manifest.json +951 -296
  15. package/dist/cli.d.ts +6 -0
  16. package/dist/cli.js +225 -148
  17. package/dist/clis/antigravity/serve.js +296 -47
  18. package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
  19. package/dist/clis/apple-podcasts/commands.test.js +76 -0
  20. package/dist/clis/apple-podcasts/search.js +2 -2
  21. package/dist/clis/apple-podcasts/top.js +9 -2
  22. package/dist/clis/arxiv/paper.js +21 -0
  23. package/dist/clis/arxiv/search.js +24 -0
  24. package/dist/clis/arxiv/utils.d.ts +18 -0
  25. package/dist/clis/arxiv/utils.js +49 -0
  26. package/dist/clis/bilibili/dynamic.js +1 -1
  27. package/dist/clis/bilibili/favorite.js +1 -1
  28. package/dist/clis/bilibili/feed.js +1 -1
  29. package/dist/clis/bilibili/following.js +1 -1
  30. package/dist/clis/bilibili/history.js +1 -1
  31. package/dist/clis/bilibili/me.js +1 -1
  32. package/dist/clis/bilibili/ranking.js +1 -1
  33. package/dist/clis/bilibili/search.js +3 -3
  34. package/dist/clis/bilibili/subtitle.js +1 -1
  35. package/dist/clis/bilibili/user-videos.js +1 -1
  36. package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
  37. package/dist/clis/bloomberg/businessweek.d.ts +1 -0
  38. package/dist/clis/bloomberg/businessweek.js +17 -0
  39. package/dist/clis/bloomberg/economics.d.ts +1 -0
  40. package/dist/clis/bloomberg/economics.js +17 -0
  41. package/dist/clis/bloomberg/feeds.d.ts +1 -0
  42. package/dist/clis/bloomberg/feeds.js +15 -0
  43. package/dist/clis/bloomberg/industries.d.ts +1 -0
  44. package/dist/clis/bloomberg/industries.js +17 -0
  45. package/dist/clis/bloomberg/main.d.ts +1 -0
  46. package/dist/clis/bloomberg/main.js +17 -0
  47. package/dist/clis/bloomberg/markets.d.ts +1 -0
  48. package/dist/clis/bloomberg/markets.js +17 -0
  49. package/dist/clis/bloomberg/news.d.ts +1 -0
  50. package/dist/clis/bloomberg/news.js +105 -0
  51. package/dist/clis/bloomberg/opinions.d.ts +1 -0
  52. package/dist/clis/bloomberg/opinions.js +17 -0
  53. package/dist/clis/bloomberg/politics.d.ts +1 -0
  54. package/dist/clis/bloomberg/politics.js +17 -0
  55. package/dist/clis/bloomberg/tech.d.ts +1 -0
  56. package/dist/clis/bloomberg/tech.js +17 -0
  57. package/dist/clis/bloomberg/utils.d.ts +34 -0
  58. package/dist/clis/bloomberg/utils.js +364 -0
  59. package/dist/clis/bloomberg/utils.test.d.ts +1 -0
  60. package/dist/clis/bloomberg/utils.test.js +129 -0
  61. package/dist/clis/boss/batchgreet.d.ts +1 -0
  62. package/dist/clis/boss/batchgreet.js +147 -0
  63. package/dist/clis/boss/chatlist.js +2 -2
  64. package/dist/clis/boss/detail.js +2 -2
  65. package/dist/clis/boss/exchange.d.ts +1 -0
  66. package/dist/clis/boss/exchange.js +111 -0
  67. package/dist/clis/boss/greet.d.ts +1 -0
  68. package/dist/clis/boss/greet.js +175 -0
  69. package/dist/clis/boss/invite.d.ts +1 -0
  70. package/dist/clis/boss/invite.js +158 -0
  71. package/dist/clis/boss/joblist.d.ts +1 -0
  72. package/dist/clis/boss/joblist.js +55 -0
  73. package/dist/clis/boss/mark.d.ts +1 -0
  74. package/dist/clis/boss/mark.js +141 -0
  75. package/dist/clis/boss/recommend.d.ts +1 -0
  76. package/dist/clis/boss/recommend.js +83 -0
  77. package/dist/clis/boss/search.js +1 -1
  78. package/dist/clis/boss/send.js +1 -1
  79. package/dist/clis/boss/stats.d.ts +1 -0
  80. package/dist/clis/boss/stats.js +116 -0
  81. package/dist/clis/chaoxing/assignments.js +1 -1
  82. package/dist/clis/chaoxing/exams.js +1 -1
  83. package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
  84. package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
  85. package/dist/clis/chaoxing/utils.test.d.ts +1 -0
  86. package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
  87. package/dist/clis/chatgpt/read.js +1 -1
  88. package/dist/clis/chatwise/export.js +1 -1
  89. package/dist/clis/chatwise/model.js +2 -2
  90. package/dist/clis/chatwise/screenshot.js +1 -1
  91. package/dist/clis/codex/export.js +1 -1
  92. package/dist/clis/codex/model.js +2 -2
  93. package/dist/clis/codex/screenshot.js +1 -1
  94. package/dist/clis/coupang/add-to-cart.js +3 -4
  95. package/dist/clis/coupang/search.js +2 -4
  96. package/dist/clis/coupang/utils.test.d.ts +1 -0
  97. package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
  98. package/dist/clis/ctrip/search.js +1 -1
  99. package/dist/clis/cursor/export.js +1 -1
  100. package/dist/clis/cursor/model.js +2 -2
  101. package/dist/clis/cursor/screenshot.js +1 -1
  102. package/dist/clis/jike/comment.js +2 -3
  103. package/dist/clis/jike/create.js +1 -2
  104. package/dist/clis/jike/feed.js +0 -1
  105. package/dist/clis/jike/like.js +1 -2
  106. package/dist/clis/jike/notifications.js +0 -1
  107. package/dist/clis/jike/post.yaml +1 -0
  108. package/dist/clis/jike/repost.js +1 -2
  109. package/dist/clis/jike/search.js +2 -3
  110. package/dist/clis/jike/topic.yaml +1 -0
  111. package/dist/clis/jike/user.yaml +1 -0
  112. package/dist/clis/jimeng/history.yaml +0 -1
  113. package/dist/clis/linkedin/search.js +7 -7
  114. package/dist/clis/linux-do/category.yaml +1 -0
  115. package/dist/clis/linux-do/search.yaml +4 -3
  116. package/dist/clis/linux-do/topic.yaml +1 -0
  117. package/dist/clis/notion/export.js +1 -1
  118. package/dist/clis/reddit/comment.js +3 -4
  119. package/dist/clis/reddit/read.js +4 -5
  120. package/dist/clis/reddit/save.js +2 -3
  121. package/dist/clis/reddit/saved.js +0 -1
  122. package/dist/clis/reddit/search.yaml +1 -0
  123. package/dist/clis/reddit/subscribe.js +0 -1
  124. package/dist/clis/reddit/upvote.js +2 -3
  125. package/dist/clis/reddit/upvoted.js +0 -1
  126. package/dist/clis/reddit/user-comments.yaml +1 -0
  127. package/dist/clis/reddit/user-posts.yaml +1 -0
  128. package/dist/clis/reddit/user.yaml +1 -0
  129. package/dist/clis/reuters/search.js +1 -1
  130. package/dist/clis/sinafinance/news.d.ts +7 -0
  131. package/dist/clis/sinafinance/news.js +61 -0
  132. package/dist/clis/smzdm/search.js +2 -3
  133. package/dist/clis/stackoverflow/search.yaml +1 -0
  134. package/dist/clis/steam/top-sellers.yaml +29 -0
  135. package/dist/clis/twitter/accept.js +2 -2
  136. package/dist/clis/twitter/article.js +2 -2
  137. package/dist/clis/twitter/block.d.ts +1 -0
  138. package/dist/clis/twitter/block.js +88 -0
  139. package/dist/clis/twitter/delete.js +1 -1
  140. package/dist/clis/twitter/hide-reply.d.ts +1 -0
  141. package/dist/clis/twitter/hide-reply.js +66 -0
  142. package/dist/clis/twitter/like.js +1 -1
  143. package/dist/clis/twitter/post.js +1 -1
  144. package/dist/clis/twitter/reply-dm.js +1 -1
  145. package/dist/clis/twitter/reply.js +2 -2
  146. package/dist/clis/twitter/search.js +1 -1
  147. package/dist/clis/twitter/thread.js +2 -2
  148. package/dist/clis/twitter/trending.d.ts +1 -0
  149. package/dist/clis/twitter/trending.js +91 -0
  150. package/dist/clis/twitter/unblock.d.ts +1 -0
  151. package/dist/clis/twitter/unblock.js +71 -0
  152. package/dist/clis/v2ex/topic.yaml +1 -0
  153. package/dist/clis/weibo/hot.js +0 -1
  154. package/dist/clis/weread/book.js +1 -1
  155. package/dist/clis/weread/highlights.js +1 -1
  156. package/dist/clis/weread/notes.js +1 -1
  157. package/dist/clis/weread/search.js +1 -1
  158. package/dist/clis/wikipedia/search.d.ts +1 -0
  159. package/dist/clis/wikipedia/search.js +30 -0
  160. package/dist/clis/wikipedia/summary.d.ts +1 -0
  161. package/dist/clis/wikipedia/summary.js +28 -0
  162. package/dist/clis/wikipedia/utils.d.ts +8 -0
  163. package/dist/clis/wikipedia/utils.js +18 -0
  164. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +79 -5
  165. package/dist/clis/xiaohongshu/creator-note-detail.js +323 -70
  166. package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
  167. package/dist/clis/xiaohongshu/creator-note-detail.test.js +258 -0
  168. package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
  169. package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
  170. package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
  171. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
  172. package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
  173. package/dist/clis/xiaohongshu/creator-notes.js +189 -71
  174. package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
  175. package/dist/clis/xiaohongshu/creator-notes.test.js +191 -0
  176. package/dist/clis/xiaohongshu/creator-profile.js +0 -1
  177. package/dist/clis/xiaohongshu/creator-stats.js +0 -1
  178. package/dist/clis/xiaohongshu/download.js +2 -3
  179. package/dist/clis/xiaohongshu/feed.yaml +0 -1
  180. package/dist/clis/xiaohongshu/notifications.yaml +0 -1
  181. package/dist/clis/xiaohongshu/search.js +2 -2
  182. package/dist/clis/xiaohongshu/user.js +1 -2
  183. package/dist/clis/yahoo-finance/quote.js +0 -1
  184. package/dist/clis/youtube/search.js +1 -1
  185. package/dist/clis/youtube/transcript.js +1 -1
  186. package/dist/clis/youtube/video.js +1 -1
  187. package/dist/clis/zhihu/download.js +1 -2
  188. package/dist/clis/zhihu/question.js +1 -1
  189. package/dist/clis/zhihu/search.yaml +4 -3
  190. package/dist/commanderAdapter.d.ts +21 -0
  191. package/dist/commanderAdapter.js +111 -0
  192. package/dist/{engine.d.ts → discovery.d.ts} +0 -6
  193. package/dist/{engine.js → discovery.js} +1 -98
  194. package/dist/download/index.d.ts +2 -6
  195. package/dist/download/index.js +19 -46
  196. package/dist/engine.test.d.ts +1 -1
  197. package/dist/engine.test.js +8 -7
  198. package/dist/execution.d.ts +22 -0
  199. package/dist/execution.js +129 -0
  200. package/dist/explore.js +121 -107
  201. package/dist/external-clis.yaml +48 -0
  202. package/dist/external.d.ts +25 -0
  203. package/dist/external.js +156 -0
  204. package/dist/main.js +1 -1
  205. package/dist/pipeline/steps/browser.js +8 -2
  206. package/dist/registry.d.ts +2 -0
  207. package/dist/registry.js +2 -0
  208. package/dist/runtime.d.ts +5 -0
  209. package/dist/runtime.js +8 -0
  210. package/dist/serialization.d.ts +34 -0
  211. package/dist/serialization.js +63 -0
  212. package/dist/types.d.ts +4 -1
  213. package/docs/.vitepress/config.mts +14 -3
  214. package/docs/adapters/browser/arxiv.md +27 -0
  215. package/docs/adapters/browser/barchart.md +32 -0
  216. package/docs/adapters/browser/bloomberg.md +70 -0
  217. package/docs/adapters/browser/chaoxing.md +39 -0
  218. package/docs/adapters/browser/grok.md +35 -0
  219. package/docs/adapters/browser/hf.md +42 -0
  220. package/docs/adapters/browser/jike.md +45 -0
  221. package/docs/adapters/browser/jimeng.md +39 -0
  222. package/docs/adapters/browser/linux-do.md +45 -0
  223. package/docs/adapters/browser/sinafinance.md +35 -0
  224. package/docs/adapters/browser/stackoverflow.md +35 -0
  225. package/docs/adapters/browser/steam.md +26 -0
  226. package/docs/adapters/browser/twitter.md +3 -0
  227. package/docs/adapters/browser/weread.md +48 -0
  228. package/docs/adapters/browser/wikipedia.md +30 -0
  229. package/docs/adapters/browser/xiaohongshu.md +5 -1
  230. package/docs/adapters/desktop/chatgpt.md +3 -3
  231. package/docs/adapters/index.md +13 -0
  232. package/docs/advanced/download.md +4 -4
  233. package/docs/developer/architecture.md +17 -4
  234. package/package.json +1 -1
  235. package/scripts/check-doc-coverage.sh +69 -0
  236. package/scripts/copy-yaml.cjs +7 -0
  237. package/src/browser/cdp.ts +9 -4
  238. package/src/browser/page.ts +7 -1
  239. package/src/build-manifest.ts +25 -19
  240. package/src/cli.ts +253 -119
  241. package/src/clis/antigravity/serve.ts +323 -50
  242. package/src/clis/apple-podcasts/commands.test.ts +95 -0
  243. package/src/clis/apple-podcasts/search.ts +2 -2
  244. package/src/clis/apple-podcasts/top.ts +12 -2
  245. package/src/clis/arxiv/paper.ts +21 -0
  246. package/src/clis/arxiv/search.ts +24 -0
  247. package/src/clis/arxiv/utils.ts +63 -0
  248. package/src/clis/bilibili/dynamic.ts +1 -1
  249. package/src/clis/bilibili/favorite.ts +1 -1
  250. package/src/clis/bilibili/feed.ts +1 -1
  251. package/src/clis/bilibili/following.ts +1 -1
  252. package/src/clis/bilibili/history.ts +1 -1
  253. package/src/clis/bilibili/me.ts +1 -1
  254. package/src/clis/bilibili/ranking.ts +1 -1
  255. package/src/clis/bilibili/search.ts +3 -3
  256. package/src/clis/bilibili/subtitle.ts +1 -1
  257. package/src/clis/bilibili/user-videos.ts +1 -1
  258. package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
  259. package/src/clis/bloomberg/businessweek.ts +18 -0
  260. package/src/clis/bloomberg/economics.ts +18 -0
  261. package/src/clis/bloomberg/feeds.ts +16 -0
  262. package/src/clis/bloomberg/industries.ts +18 -0
  263. package/src/clis/bloomberg/main.ts +18 -0
  264. package/src/clis/bloomberg/markets.ts +18 -0
  265. package/src/clis/bloomberg/news.ts +136 -0
  266. package/src/clis/bloomberg/opinions.ts +18 -0
  267. package/src/clis/bloomberg/politics.ts +18 -0
  268. package/src/clis/bloomberg/tech.ts +18 -0
  269. package/src/clis/bloomberg/utils.test.ts +135 -0
  270. package/src/clis/bloomberg/utils.ts +429 -0
  271. package/src/clis/boss/batchgreet.ts +167 -0
  272. package/src/clis/boss/chatlist.ts +2 -2
  273. package/src/clis/boss/detail.ts +2 -2
  274. package/src/clis/boss/exchange.ts +126 -0
  275. package/src/clis/boss/greet.ts +198 -0
  276. package/src/clis/boss/invite.ts +177 -0
  277. package/src/clis/boss/joblist.ts +63 -0
  278. package/src/clis/boss/mark.ts +155 -0
  279. package/src/clis/boss/recommend.ts +94 -0
  280. package/src/clis/boss/search.ts +1 -1
  281. package/src/clis/boss/send.ts +1 -1
  282. package/src/clis/boss/stats.ts +130 -0
  283. package/src/clis/chaoxing/assignments.ts +1 -1
  284. package/src/clis/chaoxing/exams.ts +1 -1
  285. package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
  286. package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
  287. package/src/clis/chatgpt/README.zh-CN.md +3 -3
  288. package/src/clis/chatgpt/read.ts +1 -1
  289. package/src/clis/chatwise/export.ts +1 -1
  290. package/src/clis/chatwise/model.ts +2 -2
  291. package/src/clis/chatwise/screenshot.ts +1 -1
  292. package/src/clis/codex/export.ts +1 -1
  293. package/src/clis/codex/model.ts +2 -2
  294. package/src/clis/codex/screenshot.ts +1 -1
  295. package/src/clis/coupang/add-to-cart.ts +3 -4
  296. package/src/clis/coupang/search.ts +2 -4
  297. package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
  298. package/src/clis/ctrip/search.ts +1 -1
  299. package/src/clis/cursor/export.ts +1 -1
  300. package/src/clis/cursor/model.ts +2 -2
  301. package/src/clis/cursor/screenshot.ts +1 -1
  302. package/src/clis/jike/comment.ts +2 -3
  303. package/src/clis/jike/create.ts +1 -2
  304. package/src/clis/jike/feed.ts +0 -1
  305. package/src/clis/jike/like.ts +1 -2
  306. package/src/clis/jike/notifications.ts +0 -1
  307. package/src/clis/jike/post.yaml +1 -0
  308. package/src/clis/jike/repost.ts +1 -2
  309. package/src/clis/jike/search.ts +2 -3
  310. package/src/clis/jike/topic.yaml +1 -0
  311. package/src/clis/jike/user.yaml +1 -0
  312. package/src/clis/jimeng/history.yaml +0 -1
  313. package/src/clis/linkedin/search.ts +7 -7
  314. package/src/clis/linux-do/category.yaml +1 -0
  315. package/src/clis/linux-do/search.yaml +4 -3
  316. package/src/clis/linux-do/topic.yaml +1 -0
  317. package/src/clis/notion/export.ts +1 -1
  318. package/src/clis/reddit/comment.ts +3 -4
  319. package/src/clis/reddit/read.ts +4 -5
  320. package/src/clis/reddit/save.ts +2 -3
  321. package/src/clis/reddit/saved.ts +0 -1
  322. package/src/clis/reddit/search.yaml +1 -0
  323. package/src/clis/reddit/subscribe.ts +0 -1
  324. package/src/clis/reddit/upvote.ts +2 -3
  325. package/src/clis/reddit/upvoted.ts +0 -1
  326. package/src/clis/reddit/user-comments.yaml +1 -0
  327. package/src/clis/reddit/user-posts.yaml +1 -0
  328. package/src/clis/reddit/user.yaml +1 -0
  329. package/src/clis/reuters/search.ts +1 -1
  330. package/src/clis/sinafinance/news.ts +76 -0
  331. package/src/clis/smzdm/search.ts +2 -3
  332. package/src/clis/stackoverflow/search.yaml +1 -0
  333. package/src/clis/steam/top-sellers.yaml +29 -0
  334. package/src/clis/twitter/accept.ts +2 -2
  335. package/src/clis/twitter/article.ts +2 -2
  336. package/src/clis/twitter/block.ts +92 -0
  337. package/src/clis/twitter/delete.ts +1 -1
  338. package/src/clis/twitter/hide-reply.ts +70 -0
  339. package/src/clis/twitter/like.ts +1 -1
  340. package/src/clis/twitter/post.ts +1 -1
  341. package/src/clis/twitter/reply-dm.ts +1 -1
  342. package/src/clis/twitter/reply.ts +2 -2
  343. package/src/clis/twitter/search.ts +1 -1
  344. package/src/clis/twitter/thread.ts +2 -2
  345. package/src/clis/twitter/trending.ts +113 -0
  346. package/src/clis/twitter/unblock.ts +75 -0
  347. package/src/clis/v2ex/topic.yaml +1 -0
  348. package/src/clis/weibo/hot.ts +0 -1
  349. package/src/clis/weread/book.ts +1 -1
  350. package/src/clis/weread/highlights.ts +1 -1
  351. package/src/clis/weread/notes.ts +1 -1
  352. package/src/clis/weread/search.ts +1 -1
  353. package/src/clis/wikipedia/search.ts +32 -0
  354. package/src/clis/wikipedia/summary.ts +28 -0
  355. package/src/clis/wikipedia/utils.ts +20 -0
  356. package/src/clis/xiaohongshu/creator-note-detail.test.ts +272 -0
  357. package/src/clis/xiaohongshu/creator-note-detail.ts +425 -73
  358. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
  359. package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
  360. package/src/clis/xiaohongshu/creator-notes.test.ts +211 -0
  361. package/src/clis/xiaohongshu/creator-notes.ts +254 -75
  362. package/src/clis/xiaohongshu/creator-profile.ts +0 -1
  363. package/src/clis/xiaohongshu/creator-stats.ts +0 -1
  364. package/src/clis/xiaohongshu/download.ts +2 -3
  365. package/src/clis/xiaohongshu/feed.yaml +0 -1
  366. package/src/clis/xiaohongshu/notifications.yaml +0 -1
  367. package/src/clis/xiaohongshu/search.ts +2 -2
  368. package/src/clis/xiaohongshu/user.ts +1 -2
  369. package/src/clis/yahoo-finance/quote.ts +0 -1
  370. package/src/clis/youtube/search.ts +1 -1
  371. package/src/clis/youtube/transcript.ts +1 -1
  372. package/src/clis/youtube/video.ts +1 -1
  373. package/src/clis/zhihu/download.ts +1 -2
  374. package/src/clis/zhihu/question.ts +1 -1
  375. package/src/clis/zhihu/search.yaml +4 -3
  376. package/src/commanderAdapter.ts +113 -0
  377. package/src/daemon.ts +3 -3
  378. package/src/{engine.ts → discovery.ts} +1 -108
  379. package/src/download/index.ts +21 -54
  380. package/src/engine.test.ts +8 -7
  381. package/src/execution.ts +138 -0
  382. package/src/explore.ts +135 -109
  383. package/src/external-clis.yaml +48 -0
  384. package/src/external.ts +185 -0
  385. package/src/main.ts +1 -1
  386. package/src/pipeline/steps/browser.ts +7 -2
  387. package/src/registry.ts +5 -0
  388. package/src/runtime.ts +9 -0
  389. package/src/serialization.ts +79 -0
  390. package/src/types.ts +1 -1
  391. package/tests/e2e/browser-public.test.ts +25 -0
  392. package/tests/e2e/public-commands.test.ts +55 -1
  393. package/dist/clis/twitter/trending.yaml +0 -46
  394. package/src/clis/twitter/trending.yaml +0 -46
  395. /package/dist/{chaoxing.test.d.ts → clis/arxiv/paper.d.ts} +0 -0
  396. /package/dist/{coupang.test.d.ts → clis/arxiv/search.d.ts} +0 -0
  397. /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
  398. /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
  399. /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
  400. /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
@@ -11,7 +11,7 @@ cli({
11
11
  domain: 'www.youtube.com',
12
12
  strategy: Strategy.COOKIE,
13
13
  args: [
14
- { name: 'url', required: true, help: 'YouTube video URL or video ID' },
14
+ { name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' },
15
15
  ],
16
16
  columns: ['field', 'value'],
17
17
  func: async (page, kwargs) => {
@@ -92,7 +92,7 @@ cli({
92
92
  domain: 'zhuanlan.zhihu.com',
93
93
  strategy: Strategy.COOKIE,
94
94
  args: [
95
- { name: 'url', required: true, help: 'Article URL (zhuanlan.zhihu.com/p/xxx)' },
95
+ { name: 'url', required: true, positional: true, help: 'Article URL (zhuanlan.zhihu.com/p/xxx)' },
96
96
  { name: 'output', default: './zhihu-articles', help: 'Output directory' },
97
97
  { name: 'download-images', type: 'boolean', default: false, help: 'Download images locally' },
98
98
  ],
@@ -104,7 +104,6 @@ cli({
104
104
 
105
105
  // Navigate to article page
106
106
  await page.goto(url);
107
- await page.wait(3);
108
107
 
109
108
  // Extract article content
110
109
  const data = await page.evaluate(`
@@ -7,7 +7,7 @@ cli({
7
7
  domain: 'www.zhihu.com',
8
8
  strategy: Strategy.COOKIE,
9
9
  args: [
10
- { name: 'id', required: true, help: 'Question ID (numeric)' },
10
+ { name: 'id', required: true, positional: true, help: 'Question ID (numeric)' },
11
11
  { name: 'limit', type: 'int', default: 5, help: 'Number of answers' },
12
12
  ],
13
13
  columns: ['rank', 'author', 'votes', 'content'],
@@ -4,10 +4,11 @@ description: 知乎搜索
4
4
  domain: www.zhihu.com
5
5
 
6
6
  args:
7
- keyword:
7
+ query:
8
+ positional: true
8
9
  type: str
9
10
  required: true
10
- description: Search keyword
11
+ description: Search query
11
12
  limit:
12
13
  type: int
13
14
  default: 10
@@ -19,7 +20,7 @@ pipeline:
19
20
  - evaluate: |
20
21
  (async () => {
21
22
  const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/<em>/g, '').replace(/<\/em>/g, '').trim();
22
- const keyword = ${{ args.keyword | json }};
23
+ const keyword = ${{ args.query | json }};
23
24
  const limit = ${{ args.limit }};
24
25
  const res = await fetch('https://www.zhihu.com/api/v4/search_v3?q=' + encodeURIComponent(keyword) + '&t=general&offset=0&limit=' + limit, {
25
26
  credentials: 'include'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Commander adapter: bridges Registry commands to Commander subcommands.
3
+ *
4
+ * This is a THIN adapter — it only handles:
5
+ * 1. Commander arg/option registration
6
+ * 2. Collecting kwargs from Commander's action args
7
+ * 3. Calling executeCommand (which handles browser sessions, validation, etc.)
8
+ * 4. Rendering output and errors
9
+ *
10
+ * All execution logic lives in execution.ts.
11
+ */
12
+
13
+ import { Command } from 'commander';
14
+ import chalk from 'chalk';
15
+ import { type CliCommand, fullName, getRegistry } from './registry.js';
16
+ import { formatRegistryHelpText } from './serialization.js';
17
+ import { render as renderOutput } from './output.js';
18
+ import { executeCommand } from './execution.js';
19
+ import { CliError } from './errors.js';
20
+
21
+ /**
22
+ * Register a single CliCommand as a Commander subcommand.
23
+ */
24
+ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): void {
25
+ if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) return;
26
+
27
+ const subCmd = siteCmd.command(cmd.name).description(cmd.description);
28
+
29
+ // Register positional args first, then named options
30
+ const positionalArgs: typeof cmd.args = [];
31
+ for (const arg of cmd.args) {
32
+ if (arg.positional) {
33
+ const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
34
+ subCmd.argument(bracket, arg.help ?? '');
35
+ positionalArgs.push(arg);
36
+ } else {
37
+ const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
38
+ if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
39
+ else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
40
+ else subCmd.option(flag, arg.help ?? '');
41
+ }
42
+ }
43
+ subCmd
44
+ .option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
45
+ .option('-v, --verbose', 'Debug output', false);
46
+
47
+ subCmd.addHelpText('after', formatRegistryHelpText(cmd));
48
+
49
+ subCmd.action(async (...actionArgs: any[]) => {
50
+ const actionOpts = actionArgs[positionalArgs.length] ?? {};
51
+ const startTime = Date.now();
52
+
53
+ // ── Collect kwargs ──────────────────────────────────────────────────
54
+ const kwargs: Record<string, any> = {};
55
+ for (let i = 0; i < positionalArgs.length; i++) {
56
+ const v = actionArgs[i];
57
+ if (v !== undefined) kwargs[positionalArgs[i].name] = v;
58
+ }
59
+ for (const arg of cmd.args) {
60
+ if (arg.positional) continue;
61
+ const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
62
+ const v = actionOpts[arg.name] ?? actionOpts[camelName];
63
+ if (v !== undefined) kwargs[arg.name] = v;
64
+ }
65
+
66
+ // ── Execute + render ────────────────────────────────────────────────
67
+ try {
68
+ if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
69
+
70
+ const result = await executeCommand(cmd, kwargs, actionOpts.verbose);
71
+
72
+ if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
73
+ console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
74
+ }
75
+ const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
76
+ renderOutput(result, {
77
+ fmt: actionOpts.format,
78
+ columns: resolved.columns,
79
+ title: `${resolved.site}/${resolved.name}`,
80
+ elapsed: (Date.now() - startTime) / 1000,
81
+ source: fullName(resolved),
82
+ footerExtra: resolved.footerExtra?.(kwargs),
83
+ });
84
+ } catch (err: any) {
85
+ if (err instanceof CliError) {
86
+ console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
87
+ if (err.hint) console.error(chalk.yellow(`Hint: ${err.hint}`));
88
+ } else if (actionOpts.verbose && err.stack) {
89
+ console.error(chalk.red(err.stack));
90
+ } else {
91
+ console.error(chalk.red(`Error: ${err.message ?? err}`));
92
+ }
93
+ process.exitCode = 1;
94
+ }
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Register all commands from the registry onto a Commander program.
100
+ */
101
+ export function registerAllCommands(
102
+ program: Command,
103
+ siteGroups: Map<string, Command>,
104
+ ): void {
105
+ for (const [, cmd] of getRegistry()) {
106
+ let siteCmd = siteGroups.get(cmd.site);
107
+ if (!siteCmd) {
108
+ siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
109
+ siteGroups.set(cmd.site, siteCmd);
110
+ }
111
+ registerCommandToProgram(siteCmd, cmd);
112
+ }
113
+ }
package/src/daemon.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
15
- import { WebSocketServer, WebSocket } from 'ws';
15
+ import { WebSocketServer, WebSocket, type RawData } from 'ws';
16
16
 
17
17
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
18
18
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
@@ -138,11 +138,11 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
138
138
  const httpServer = createServer((req, res) => { handleRequest(req, res).catch(() => { res.writeHead(500); res.end(); }); });
139
139
  const wss = new WebSocketServer({ server: httpServer, path: '/ext' });
140
140
 
141
- wss.on('connection', (ws) => {
141
+ wss.on('connection', (ws: WebSocket) => {
142
142
  console.error('[daemon] Extension connected');
143
143
  extensionWs = ws;
144
144
 
145
- ws.on('message', (data) => {
145
+ ws.on('message', (data: RawData) => {
146
146
  try {
147
147
  const msg = JSON.parse(data.toString());
148
148
 
@@ -12,13 +12,7 @@ import * as fs from 'node:fs';
12
12
  import * as path from 'node:path';
13
13
  import yaml from 'js-yaml';
14
14
  import { type CliCommand, type InternalCliCommand, type Arg, Strategy, registerCommand } from './registry.js';
15
- import type { IPage } from './types.js';
16
- import { executePipeline } from './pipeline.js';
17
15
  import { log } from './logger.js';
18
- import { AdapterLoadError } from './errors.js';
19
-
20
- /** Set of TS module paths that have been loaded */
21
- const _loadedModules = new Set<string>();
22
16
 
23
17
  /**
24
18
  * Discover and register CLI commands.
@@ -145,6 +139,7 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
145
139
  type: argDef?.type ?? 'str',
146
140
  default: argDef?.default,
147
141
  required: argDef?.required ?? false,
142
+ positional: argDef?.positional ?? false,
148
143
  help: argDef?.description ?? argDef?.help ?? '',
149
144
  choices: argDef?.choices,
150
145
  });
@@ -170,105 +165,3 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
170
165
  log.warn(`Failed to load ${filePath}: ${err.message}`);
171
166
  }
172
167
  }
173
-
174
- /**
175
- * Validates and coerces arguments based on the command's Arg definitions.
176
- */
177
- function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: Record<string, any>): Record<string, any> {
178
- const result: Record<string, any> = { ...kwargs };
179
-
180
- for (const argDef of cmdArgs) {
181
- const val = result[argDef.name];
182
-
183
- // 1. Check required
184
- if (argDef.required && (val === undefined || val === null || val === '')) {
185
- throw new Error(`Argument "${argDef.name}" is required.\n${argDef.help ? `Hint: ${argDef.help}` : ''}`);
186
- }
187
-
188
- if (val !== undefined && val !== null) {
189
- // 2. Type coercion
190
- if (argDef.type === 'int' || argDef.type === 'number') {
191
- const num = Number(val);
192
- if (Number.isNaN(num)) {
193
- throw new Error(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`);
194
- }
195
- result[argDef.name] = num;
196
- } else if (argDef.type === 'boolean' || argDef.type === 'bool') {
197
- if (typeof val === 'string') {
198
- const lower = val.toLowerCase();
199
- if (lower === 'true' || lower === '1') result[argDef.name] = true;
200
- else if (lower === 'false' || lower === '0') result[argDef.name] = false;
201
- else throw new Error(`Argument "${argDef.name}" must be a boolean (true/false). Received: "${val}"`);
202
- } else {
203
- result[argDef.name] = Boolean(val);
204
- }
205
- }
206
-
207
- // 3. Choices validation
208
- const coercedVal = result[argDef.name];
209
- if (argDef.choices && argDef.choices.length > 0) {
210
- // Only stringent check for string/number types against choices array
211
- if (!argDef.choices.map(String).includes(String(coercedVal))) {
212
- throw new Error(`Argument "${argDef.name}" must be one of: ${argDef.choices.join(', ')}. Received: "${coercedVal}"`);
213
- }
214
- }
215
- } else if (argDef.default !== undefined) {
216
- // Set default if value is missing
217
- result[argDef.name] = argDef.default;
218
- }
219
- }
220
- return result;
221
- }
222
-
223
- /**
224
- * Execute a CLI command. Handles lazy-loading of TS modules.
225
- */
226
- export async function executeCommand(
227
- cmd: CliCommand,
228
- page: IPage | null,
229
- rawKwargs: Record<string, any>,
230
- debug: boolean = false,
231
- ): Promise<any> {
232
- let kwargs: Record<string, any>;
233
- try {
234
- kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
235
- } catch (err: any) {
236
- // Re-throw validation errors clearly
237
- throw new Error(`[Argument Validation Error]\n${err.message}`);
238
- }
239
-
240
- // Lazy-load TS module on first execution
241
- const internal = cmd as InternalCliCommand;
242
- if (internal._lazy && internal._modulePath) {
243
- const modulePath = internal._modulePath;
244
- if (!_loadedModules.has(modulePath)) {
245
- try {
246
- await import(`file://${modulePath}`);
247
- _loadedModules.add(modulePath);
248
- } catch (err: any) {
249
- throw new AdapterLoadError(
250
- `Failed to load adapter module ${modulePath}: ${err.message}`,
251
- 'Check that the adapter file exists and has no syntax errors.',
252
- );
253
- }
254
- }
255
- // After loading, the module's cli() call will have updated the registry
256
- // with the real func/pipeline. Re-fetch the command.
257
- const { getRegistry, fullName } = await import('./registry.js');
258
- const updated = getRegistry().get(fullName(cmd));
259
- if (updated && updated.func) {
260
- return updated.func(page!, kwargs, debug);
261
- }
262
- if (updated && updated.pipeline) {
263
- return executePipeline(page, updated.pipeline, { args: kwargs, debug });
264
- }
265
- }
266
-
267
- if (cmd.func) {
268
- return cmd.func(page!, kwargs, debug);
269
- }
270
- if (cmd.pipeline) {
271
- return executePipeline(page, cmd.pipeline, { args: kwargs, debug });
272
- }
273
- throw new Error(`Command ${cmd.site}/${cmd.name} has no func or pipeline`);
274
- }
@@ -2,7 +2,7 @@
2
2
  * Download utilities: HTTP downloads, yt-dlp wrapper, format conversion.
3
3
  */
4
4
 
5
- import { spawn, execSync } from 'node:child_process';
5
+ import { spawn } from 'node:child_process';
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
  import * as https from 'node:https';
@@ -10,6 +10,7 @@ import * as http from 'node:http';
10
10
  import * as os from 'node:os';
11
11
  import { URL } from 'node:url';
12
12
  import type { ProgressBar } from './progress.js';
13
+ import { isBinaryInstalled } from '../external.js';
13
14
 
14
15
  export interface DownloadOptions {
15
16
  cookies?: string;
@@ -36,68 +37,43 @@ export interface BrowserCookie {
36
37
  expirationDate?: number;
37
38
  }
38
39
 
39
- /**
40
- * Check if yt-dlp is available in PATH.
41
- */
40
+ /** Check if yt-dlp is available in PATH. */
42
41
  export function checkYtdlp(): boolean {
43
- try {
44
- execSync('yt-dlp --version', { encoding: 'utf-8', stdio: 'pipe' });
45
- return true;
46
- } catch {
47
- return false;
48
- }
42
+ return isBinaryInstalled('yt-dlp');
49
43
  }
50
44
 
51
- /**
52
- * Check if ffmpeg is available in PATH.
53
- */
45
+ /** Check if ffmpeg is available in PATH. */
54
46
  export function checkFfmpeg(): boolean {
55
- try {
56
- execSync('ffmpeg -version', { encoding: 'utf-8', stdio: 'pipe' });
57
- return true;
58
- } catch {
59
- return false;
60
- }
47
+ return isBinaryInstalled('ffmpeg');
61
48
  }
62
49
 
50
+ /** Domains that host video content and can be downloaded via yt-dlp. */
51
+ const VIDEO_PLATFORM_DOMAINS = [
52
+ 'youtube.com', 'youtu.be', 'bilibili.com', 'twitter.com',
53
+ 'x.com', 'tiktok.com', 'vimeo.com', 'twitch.tv',
54
+ ];
55
+
56
+ const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif']);
57
+ const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.avi', '.mov', '.mkv', '.flv', '.m3u8', '.ts']);
58
+ const DOC_EXTENSIONS = new Set(['.html', '.htm', '.json', '.xml', '.txt', '.md', '.markdown']);
59
+
63
60
  /**
64
61
  * Detect content type from URL and optional headers.
65
62
  */
66
63
  export function detectContentType(url: string, contentType?: string): 'image' | 'video' | 'document' | 'binary' {
67
- // Check content-type header first
68
64
  if (contentType) {
69
65
  if (contentType.startsWith('image/')) return 'image';
70
66
  if (contentType.startsWith('video/')) return 'video';
71
67
  if (contentType.startsWith('text/') || contentType.includes('json') || contentType.includes('xml')) return 'document';
72
68
  }
73
69
 
74
- // Detect from URL
75
70
  const urlLower = url.toLowerCase();
76
71
  const ext = path.extname(new URL(url).pathname).toLowerCase();
77
72
 
78
- // Image extensions
79
- if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif'].includes(ext)) {
80
- return 'image';
81
- }
82
-
83
- // Video extensions
84
- if (['.mp4', '.webm', '.avi', '.mov', '.mkv', '.flv', '.m3u8', '.ts'].includes(ext)) {
85
- return 'video';
86
- }
87
-
88
- // Video platforms (need yt-dlp)
89
- if (urlLower.includes('youtube.com') || urlLower.includes('youtu.be') ||
90
- urlLower.includes('bilibili.com') || urlLower.includes('twitter.com') ||
91
- urlLower.includes('x.com') || urlLower.includes('tiktok.com') ||
92
- urlLower.includes('vimeo.com') || urlLower.includes('twitch.tv')) {
93
- return 'video';
94
- }
95
-
96
- // Document extensions
97
- if (['.html', '.htm', '.json', '.xml', '.txt', '.md', '.markdown'].includes(ext)) {
98
- return 'document';
99
- }
100
-
73
+ if (IMAGE_EXTENSIONS.has(ext)) return 'image';
74
+ if (VIDEO_EXTENSIONS.has(ext)) return 'video';
75
+ if (VIDEO_PLATFORM_DOMAINS.some(d => urlLower.includes(d))) return 'video';
76
+ if (DOC_EXTENSIONS.has(ext)) return 'document';
101
77
  return 'binary';
102
78
  }
103
79
 
@@ -106,16 +82,7 @@ export function detectContentType(url: string, contentType?: string): 'image' |
106
82
  */
107
83
  export function requiresYtdlp(url: string): boolean {
108
84
  const urlLower = url.toLowerCase();
109
- return (
110
- urlLower.includes('youtube.com') ||
111
- urlLower.includes('youtu.be') ||
112
- urlLower.includes('bilibili.com/video') ||
113
- urlLower.includes('twitter.com') ||
114
- urlLower.includes('x.com') ||
115
- urlLower.includes('tiktok.com') ||
116
- urlLower.includes('vimeo.com') ||
117
- urlLower.includes('twitch.tv')
118
- );
85
+ return VIDEO_PLATFORM_DOMAINS.some(d => urlLower.includes(d));
119
86
  }
120
87
 
121
88
  /**
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Tests for engine.ts: CLI discovery and command execution.
2
+ * Tests for discovery and execution modules.
3
3
  */
4
4
 
5
5
  import { describe, it, expect, vi, beforeEach } from 'vitest';
6
- import { discoverClis, executeCommand } from './engine.js';
6
+ import { discoverClis } from './discovery.js';
7
+ import { executeCommand } from './execution.js';
7
8
  import { getRegistry, cli, Strategy } from './registry.js';
8
9
 
9
10
  describe('discoverClis', () => {
@@ -27,7 +28,7 @@ describe('executeCommand', () => {
27
28
  func: async (_page, kwargs) => [{ noteId: kwargs['note-id'] }],
28
29
  });
29
30
 
30
- const result = await executeCommand(cmd, null, { 'note-id': 'abc123' });
31
+ const result = await executeCommand(cmd, { 'note-id': 'abc123' });
31
32
  expect(result).toEqual([{ noteId: 'abc123' }]);
32
33
  });
33
34
 
@@ -43,7 +44,7 @@ describe('executeCommand', () => {
43
44
  },
44
45
  });
45
46
 
46
- const result = await executeCommand(cmd, null, { query: 'hello' });
47
+ const result = await executeCommand(cmd, { query: 'hello' });
47
48
  expect(result).toEqual([{ title: 'hello' }]);
48
49
  });
49
50
 
@@ -61,7 +62,7 @@ describe('executeCommand', () => {
61
62
  });
62
63
 
63
64
  // Pipeline commands require page for evaluate step, so we'll test the error path
64
- await expect(executeCommand(cmd, null, {})).rejects.toThrow();
65
+ await expect(executeCommand(cmd, {})).rejects.toThrow();
65
66
  });
66
67
 
67
68
  it('throws for command with no func or pipeline', async () => {
@@ -72,7 +73,7 @@ describe('executeCommand', () => {
72
73
  browser: false,
73
74
  });
74
75
 
75
- await expect(executeCommand(cmd, null, {})).rejects.toThrow('has no func or pipeline');
76
+ await expect(executeCommand(cmd, {})).rejects.toThrow('has no func or pipeline');
76
77
  });
77
78
 
78
79
  it('passes debug flag to func', async () => {
@@ -88,7 +89,7 @@ describe('executeCommand', () => {
88
89
  },
89
90
  });
90
91
 
91
- await executeCommand(cmd, null, {}, true);
92
+ await executeCommand(cmd, {}, true);
92
93
  expect(receivedDebug).toBe(true);
93
94
  });
94
95
  });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Command execution: validates args, manages browser sessions, runs commands.
3
+ *
4
+ * This is the single entry point for executing any CLI command. It handles:
5
+ * 1. Argument validation and coercion
6
+ * 2. Browser session lifecycle (if needed)
7
+ * 3. Domain pre-navigation for cookie/header strategies
8
+ * 4. Timeout enforcement
9
+ * 5. Lazy-loading of TS modules from manifest
10
+ */
11
+
12
+ import { type CliCommand, type InternalCliCommand, type Arg, Strategy, getRegistry, fullName } from './registry.js';
13
+ import type { IPage } from './types.js';
14
+ import { executePipeline } from './pipeline.js';
15
+ import { AdapterLoadError } from './errors.js';
16
+ import { shouldUseBrowserSession } from './capabilityRouting.js';
17
+ import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
18
+
19
+ /** Set of TS module paths that have been loaded */
20
+ const _loadedModules = new Set<string>();
21
+
22
+ /**
23
+ * Validates and coerces arguments based on the command's Arg definitions.
24
+ */
25
+ export function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: Record<string, any>): Record<string, any> {
26
+ const result: Record<string, any> = { ...kwargs };
27
+
28
+ for (const argDef of cmdArgs) {
29
+ const val = result[argDef.name];
30
+
31
+ // 1. Check required
32
+ if (argDef.required && (val === undefined || val === null || val === '')) {
33
+ throw new Error(`Argument "${argDef.name}" is required.\n${argDef.help ? `Hint: ${argDef.help}` : ''}`);
34
+ }
35
+
36
+ if (val !== undefined && val !== null) {
37
+ // 2. Type coercion
38
+ if (argDef.type === 'int' || argDef.type === 'number') {
39
+ const num = Number(val);
40
+ if (Number.isNaN(num)) {
41
+ throw new Error(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`);
42
+ }
43
+ result[argDef.name] = num;
44
+ } else if (argDef.type === 'boolean' || argDef.type === 'bool') {
45
+ if (typeof val === 'string') {
46
+ const lower = val.toLowerCase();
47
+ if (lower === 'true' || lower === '1') result[argDef.name] = true;
48
+ else if (lower === 'false' || lower === '0') result[argDef.name] = false;
49
+ else throw new Error(`Argument "${argDef.name}" must be a boolean (true/false). Received: "${val}"`);
50
+ } else {
51
+ result[argDef.name] = Boolean(val);
52
+ }
53
+ }
54
+
55
+ // 3. Choices validation
56
+ const coercedVal = result[argDef.name];
57
+ if (argDef.choices && argDef.choices.length > 0) {
58
+ if (!argDef.choices.map(String).includes(String(coercedVal))) {
59
+ throw new Error(`Argument "${argDef.name}" must be one of: ${argDef.choices.join(', ')}. Received: "${coercedVal}"`);
60
+ }
61
+ }
62
+ } else if (argDef.default !== undefined) {
63
+ result[argDef.name] = argDef.default;
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Run a command's func or pipeline against a page.
71
+ */
72
+ async function runCommand(
73
+ cmd: CliCommand,
74
+ page: IPage | null,
75
+ kwargs: Record<string, any>,
76
+ debug: boolean,
77
+ ): Promise<any> {
78
+ // Lazy-load TS module on first execution (manifest fast-path)
79
+ const internal = cmd as InternalCliCommand;
80
+ if (internal._lazy && internal._modulePath) {
81
+ const modulePath = internal._modulePath;
82
+ if (!_loadedModules.has(modulePath)) {
83
+ try {
84
+ await import(`file://${modulePath}`);
85
+ _loadedModules.add(modulePath);
86
+ } catch (err: any) {
87
+ throw new AdapterLoadError(
88
+ `Failed to load adapter module ${modulePath}: ${err.message}`,
89
+ 'Check that the adapter file exists and has no syntax errors.',
90
+ );
91
+ }
92
+ }
93
+ // After loading, the module's cli() call will have updated the registry.
94
+ const updated = getRegistry().get(fullName(cmd));
95
+ if (updated?.func) return updated.func(page!, kwargs, debug);
96
+ if (updated?.pipeline) return executePipeline(page, updated.pipeline, { args: kwargs, debug });
97
+ }
98
+
99
+ if (cmd.func) return cmd.func(page!, kwargs, debug);
100
+ if (cmd.pipeline) return executePipeline(page, cmd.pipeline, { args: kwargs, debug });
101
+ throw new Error(`Command ${fullName(cmd)} has no func or pipeline`);
102
+ }
103
+
104
+ /**
105
+ * Execute a CLI command. Automatically manages browser sessions when needed.
106
+ *
107
+ * This is the unified entry point — callers don't need to care about
108
+ * whether the command requires a browser or not.
109
+ */
110
+ export async function executeCommand(
111
+ cmd: CliCommand,
112
+ rawKwargs: Record<string, any>,
113
+ debug: boolean = false,
114
+ ): Promise<any> {
115
+ let kwargs: Record<string, any>;
116
+ try {
117
+ kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
118
+ } catch (err: any) {
119
+ throw new Error(`[Argument Validation Error]\n${err.message}`);
120
+ }
121
+
122
+ if (shouldUseBrowserSession(cmd)) {
123
+ const BrowserFactory = getBrowserFactory();
124
+ return browserSession(BrowserFactory, async (page) => {
125
+ // Cookie/header strategies require same-origin context for credentialed fetch.
126
+ if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
127
+ try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
128
+ }
129
+ return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
130
+ timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT,
131
+ label: fullName(cmd),
132
+ });
133
+ }, { workspace: `site:${cmd.site}` });
134
+ }
135
+
136
+ // Non-browser commands run directly
137
+ return runCommand(cmd, null, kwargs, debug);
138
+ }