@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
package/dist/execution.js CHANGED
@@ -19,7 +19,7 @@ import { emitHook } from './hooks.js';
19
19
  import { checkDaemonStatus } from './browser/discover.js';
20
20
  import { log } from './logger.js';
21
21
  import { isElectronApp } from './electron-apps.js';
22
- import { resolveElectronEndpoint } from './launcher.js';
22
+ import { probeCDP, resolveElectronEndpoint } from './launcher.js';
23
23
  const _loadedModules = new Set();
24
24
  export function coerceAndValidateArgs(cmdArgs, kwargs) {
25
25
  const result = { ...kwargs };
@@ -111,29 +111,11 @@ function ensureRequiredEnv(cmd) {
111
111
  return;
112
112
  throw new CommandExecutionError(`Command ${fullName(cmd)} requires environment variable ${missing.name}.`, missing.help ?? `Set ${missing.name} before running ${fullName(cmd)}.`);
113
113
  }
114
- /**
115
- * Check if the browser is already on the target domain, avoiding redundant navigation.
116
- * Returns true if current page hostname matches the pre-nav URL hostname.
117
- */
118
- async function isAlreadyOnDomain(page, targetUrl) {
119
- if (!page.getCurrentUrl)
120
- return false;
121
- try {
122
- const currentUrl = await page.getCurrentUrl();
123
- if (!currentUrl)
124
- return false;
125
- const currentHost = new URL(currentUrl).hostname;
126
- const targetHost = new URL(targetUrl).hostname;
127
- return currentHost === targetHost;
128
- }
129
- catch {
130
- return false;
131
- }
132
- }
133
114
  export async function executeCommand(cmd, rawKwargs, debug = false) {
134
115
  let kwargs;
135
116
  try {
136
117
  kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
118
+ cmd.validateArgs?.(kwargs);
137
119
  }
138
120
  catch (err) {
139
121
  if (err instanceof ArgumentError)
@@ -152,8 +134,18 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
152
134
  const electron = isElectronApp(cmd.site);
153
135
  let cdpEndpoint;
154
136
  if (electron) {
155
- // Electron apps: auto-detect, prompt restart if needed, launch with CDP
156
- cdpEndpoint = await resolveElectronEndpoint(cmd.site);
137
+ // Electron apps: respect manual endpoint override, then try auto-detect
138
+ const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
139
+ if (manualEndpoint) {
140
+ const port = Number(new URL(manualEndpoint).port);
141
+ if (!await probeCDP(port)) {
142
+ throw new CommandExecutionError(`CDP not reachable at ${manualEndpoint}`, 'Check that the app is running with --remote-debugging-port and the endpoint is correct.');
143
+ }
144
+ cdpEndpoint = manualEndpoint;
145
+ }
146
+ else {
147
+ cdpEndpoint = await resolveElectronEndpoint(cmd.site);
148
+ }
157
149
  }
158
150
  else {
159
151
  // Browser Bridge: fail-fast when daemon is up but extension is missing.
@@ -162,7 +154,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
162
154
  if (status.running && !status.extensionConnected) {
163
155
  throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
164
156
  ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
165
- ' 2. chrome://extensions → Developer Mode → Load unpacked\n' +
157
+ ' 2. In Chrome or Chromium, open chrome://extensions → Developer Mode → Load unpacked\n' +
166
158
  ' Then run: opencli doctor');
167
159
  }
168
160
  }
@@ -171,19 +163,17 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
171
163
  result = await browserSession(BrowserFactory, async (page) => {
172
164
  const preNavUrl = resolvePreNav(cmd);
173
165
  if (preNavUrl) {
174
- const skip = await isAlreadyOnDomain(page, preNavUrl);
175
- if (skip) {
176
- if (debug)
177
- log.debug('[pre-nav] Already on target domain, skipping navigation');
166
+ // Navigate directly the extension's handleNavigate already has a fast-path
167
+ // that skips navigation if the tab is already at the target URL.
168
+ // This avoids an extra exec round-trip (getCurrentUrl) on first command and
169
+ // lets the extension create the automation window with the target URL directly
170
+ // instead of about:blank.
171
+ try {
172
+ await page.goto(preNavUrl);
178
173
  }
179
- else {
180
- try {
181
- await page.goto(preNavUrl);
182
- }
183
- catch (err) {
184
- if (debug)
185
- log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
186
- }
174
+ catch (err) {
175
+ if (debug)
176
+ log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
187
177
  }
188
178
  }
189
179
  return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
package/dist/explore.js CHANGED
@@ -263,7 +263,7 @@ export async function exploreUrl(url, opts) {
263
263
  await page.wait(2); // wait for XHRs to settle
264
264
  }
265
265
  catch (e) {
266
- log.debug(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
266
+ log.verbose(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
267
267
  }
268
268
  }
269
269
  // Step 3: Read page metadata
@@ -8,6 +8,7 @@
8
8
  * 4. Launch with --remote-debugging-port
9
9
  * 5. Poll /json until ready
10
10
  */
11
+ import type { ElectronAppEntry } from './electron-apps.js';
11
12
  /**
12
13
  * Probe whether a CDP endpoint is listening on the given port.
13
14
  * Returns true if http://127.0.0.1:{port}/json responds successfully.
@@ -28,6 +29,9 @@ export declare function killProcess(processName: string): void;
28
29
  * Returns null if the app is not installed.
29
30
  */
30
31
  export declare function discoverAppPath(displayName: string): string | null;
32
+ export declare function resolveExecutableCandidates(appPath: string, app: ElectronAppEntry): string[];
33
+ export declare function launchDetachedApp(executable: string, args: string[], label: string): Promise<void>;
34
+ export declare function launchElectronApp(appPath: string, app: ElectronAppEntry, args: string[], label: string): Promise<void>;
31
35
  /**
32
36
  * Main entry point: resolve an Electron app to a CDP endpoint URL.
33
37
  *
package/dist/launcher.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { execFileSync, spawn } from 'node:child_process';
12
12
  import { request as httpRequest } from 'node:http';
13
+ import * as path from 'node:path';
13
14
  import { getElectronApp } from './electron-apps.js';
14
15
  import { confirmPrompt } from './tui.js';
15
16
  import { CommandExecutionError } from './errors.js';
@@ -38,6 +39,8 @@ export function probeCDP(port, timeoutMs = PROBE_TIMEOUT_MS) {
38
39
  * Uses pgrep on macOS/Linux.
39
40
  */
40
41
  export function detectProcess(processName) {
42
+ if (process.platform === 'win32')
43
+ return false; // pgrep not available on Windows
41
44
  try {
42
45
  execFileSync('pgrep', ['-x', processName], { encoding: 'utf-8', stdio: 'pipe' });
43
46
  return true;
@@ -50,6 +53,8 @@ export function detectProcess(processName) {
50
53
  * Kill a process by name. Sends SIGTERM first, then SIGKILL after grace period.
51
54
  */
52
55
  export function killProcess(processName) {
56
+ if (process.platform === 'win32')
57
+ return; // pkill not available on Windows
53
58
  try {
54
59
  execFileSync('pkill', ['-x', processName], { stdio: 'pipe' });
55
60
  }
@@ -91,6 +96,57 @@ export function discoverAppPath(displayName) {
91
96
  function resolveExecutable(appPath, processName) {
92
97
  return `${appPath}/Contents/MacOS/${processName}`;
93
98
  }
99
+ function isMissingExecutableError(err, label) {
100
+ return err instanceof CommandExecutionError
101
+ && err.message.startsWith(`Could not launch ${label}: executable not found at `);
102
+ }
103
+ export function resolveExecutableCandidates(appPath, app) {
104
+ const executableNames = app.executableNames?.length ? app.executableNames : [app.processName];
105
+ return [...new Set(executableNames)].map((name) => resolveExecutable(appPath, name));
106
+ }
107
+ export async function launchDetachedApp(executable, args, label) {
108
+ await new Promise((resolve, reject) => {
109
+ const child = spawn(executable, args, {
110
+ detached: true,
111
+ stdio: 'ignore',
112
+ });
113
+ const onError = (err) => {
114
+ if (err.code === 'ENOENT') {
115
+ reject(new CommandExecutionError(`Could not launch ${label}: executable not found at ${executable}`, `Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`));
116
+ return;
117
+ }
118
+ reject(new CommandExecutionError(`Failed to launch ${label}`, err.message));
119
+ };
120
+ child.once('error', onError);
121
+ child.once('spawn', () => {
122
+ child.off('error', onError);
123
+ child.unref();
124
+ resolve();
125
+ });
126
+ });
127
+ }
128
+ export async function launchElectronApp(appPath, app, args, label) {
129
+ const executables = resolveExecutableCandidates(appPath, app);
130
+ let lastMissingExecutableError;
131
+ for (const executable of executables) {
132
+ log.debug(`[launcher] Launching: ${executable} ${args.join(' ')}`);
133
+ try {
134
+ await launchDetachedApp(executable, args, label);
135
+ return;
136
+ }
137
+ catch (err) {
138
+ if (isMissingExecutableError(err, label)) {
139
+ lastMissingExecutableError = err;
140
+ continue;
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+ if (executables.length > 1) {
146
+ throw new CommandExecutionError(`Could not launch ${label}: no compatible executable found in ${path.join(appPath, 'Contents', 'MacOS')}`, `Tried: ${executables.map((executable) => path.basename(executable)).join(', ')}. Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`);
147
+ }
148
+ throw lastMissingExecutableError ?? new CommandExecutionError(`Could not launch ${label}`, `Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`);
149
+ }
94
150
  async function pollForReady(port) {
95
151
  const deadline = Date.now() + POLL_TIMEOUT_MS;
96
152
  while (Date.now() < deadline) {
@@ -119,7 +175,13 @@ export async function resolveElectronEndpoint(site) {
119
175
  log.debug(`[launcher] CDP already available on port ${port}`);
120
176
  return endpoint;
121
177
  }
122
- // Step 2: Running without CDP?
178
+ // Step 2: Running without CDP? (process detection requires Unix tools)
179
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
180
+ throw new CommandExecutionError(`${label} is not reachable on CDP port ${port}.`, `Auto-launch is not yet supported on ${process.platform}.\n` +
181
+ `Start ${label} manually with --remote-debugging-port=${port}, then either:\n` +
182
+ ` • Set OPENCLI_CDP_ENDPOINT=http://127.0.0.1:${port}\n` +
183
+ ` • Or just re-run the command once ${label} is listening on port ${port}.`);
184
+ }
123
185
  const isRunning = detectProcess(processName);
124
186
  if (isRunning) {
125
187
  log.debug(`[launcher] ${label} is running but CDP not available`);
@@ -136,14 +198,8 @@ export async function resolveElectronEndpoint(site) {
136
198
  throw new CommandExecutionError(`Could not find ${label} on this machine.`, `Install ${label} or register a custom path in ~/.opencli/apps.yaml`);
137
199
  }
138
200
  // Step 4: Launch
139
- const executable = resolveExecutable(appPath, processName);
140
201
  const args = [`--remote-debugging-port=${port}`, ...(app.extraArgs ?? [])];
141
- log.debug(`[launcher] Launching: ${executable} ${args.join(' ')}`);
142
- const child = spawn(executable, args, {
143
- detached: true,
144
- stdio: 'ignore',
145
- });
146
- child.unref();
202
+ await launchElectronApp(appPath, app, args, label);
147
203
  // Step 5: Poll for readiness
148
204
  process.stderr.write(` Waiting for ${label} on port ${port}...\n`);
149
205
  await pollForReady(port);
@@ -1,12 +1,24 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { probeCDP, detectProcess, discoverAppPath } from './launcher.js';
2
+ import { detectProcess, discoverAppPath, launchDetachedApp, launchElectronApp, probeCDP, resolveExecutableCandidates } from './launcher.js';
3
+ function createMockChildProcess() {
4
+ const listeners = new Map();
5
+ return {
6
+ once: vi.fn((event, handler) => {
7
+ listeners.set(event, [...(listeners.get(event) ?? []), handler]);
8
+ }),
9
+ off: vi.fn((event, handler) => {
10
+ listeners.set(event, (listeners.get(event) ?? []).filter((listener) => listener !== handler));
11
+ }),
12
+ unref: vi.fn(),
13
+ emit: (event, value) => {
14
+ for (const listener of listeners.get(event) ?? [])
15
+ listener(value);
16
+ },
17
+ };
18
+ }
3
19
  vi.mock('node:child_process', () => ({
4
20
  execFileSync: vi.fn(),
5
- spawn: vi.fn(() => ({
6
- unref: vi.fn(),
7
- pid: 12345,
8
- on: vi.fn(),
9
- })),
21
+ spawn: vi.fn(),
10
22
  }));
11
23
  const cp = vi.mocked(await import('node:child_process'));
12
24
  describe('probeCDP', () => {
@@ -28,7 +40,7 @@ describe('detectProcess', () => {
28
40
  const result = detectProcess('NonExistentApp');
29
41
  expect(result).toBe(false);
30
42
  });
31
- it('returns true when pgrep finds a process', () => {
43
+ it.skipIf(process.platform === 'win32')('returns true when pgrep finds a process', () => {
32
44
  cp.execFileSync.mockReturnValue('12345\n');
33
45
  const result = detectProcess('Cursor');
34
46
  expect(result).toBe(true);
@@ -55,3 +67,72 @@ describe('discoverAppPath', () => {
55
67
  expect(result).toBeNull();
56
68
  });
57
69
  });
70
+ describe('launchDetachedApp', () => {
71
+ beforeEach(() => {
72
+ vi.restoreAllMocks();
73
+ cp.spawn.mockReset();
74
+ });
75
+ it('unrefs the process after spawn succeeds', async () => {
76
+ const child = createMockChildProcess();
77
+ cp.spawn.mockImplementation(() => {
78
+ queueMicrotask(() => child.emit('spawn'));
79
+ return child;
80
+ });
81
+ await expect(launchDetachedApp('/Applications/Antigravity.app/Contents/MacOS/Antigravity', ['--remote-debugging-port=9234'], 'Antigravity'))
82
+ .resolves
83
+ .toBeUndefined();
84
+ expect(child.unref).toHaveBeenCalledTimes(1);
85
+ });
86
+ it('converts ENOENT into a controlled launch error', async () => {
87
+ const child = createMockChildProcess();
88
+ cp.spawn.mockImplementation(() => {
89
+ queueMicrotask(() => child.emit('error', Object.assign(new Error('missing binary'), { code: 'ENOENT' })));
90
+ return child;
91
+ });
92
+ await expect(launchDetachedApp('/Applications/Antigravity.app/Contents/MacOS/Antigravity', ['--remote-debugging-port=9234'], 'Antigravity'))
93
+ .rejects
94
+ .toThrow('Could not launch Antigravity');
95
+ expect(child.unref).not.toHaveBeenCalled();
96
+ });
97
+ });
98
+ describe('resolveExecutableCandidates', () => {
99
+ it('prefers explicit executable candidates over processName', () => {
100
+ const app = {
101
+ port: 9234,
102
+ processName: 'Antigravity',
103
+ executableNames: ['Electron', 'Antigravity'],
104
+ };
105
+ expect(resolveExecutableCandidates('/Applications/Antigravity.app', app)).toEqual([
106
+ '/Applications/Antigravity.app/Contents/MacOS/Electron',
107
+ '/Applications/Antigravity.app/Contents/MacOS/Antigravity',
108
+ ]);
109
+ });
110
+ });
111
+ describe('launchElectronApp', () => {
112
+ beforeEach(() => {
113
+ vi.restoreAllMocks();
114
+ cp.spawn.mockReset();
115
+ });
116
+ it('falls back to the next executable candidate when the first is missing', async () => {
117
+ const firstChild = createMockChildProcess();
118
+ const secondChild = createMockChildProcess();
119
+ const app = {
120
+ port: 9234,
121
+ processName: 'Antigravity',
122
+ executableNames: ['Electron', 'Antigravity'],
123
+ };
124
+ cp.spawn
125
+ .mockImplementationOnce(() => {
126
+ queueMicrotask(() => firstChild.emit('error', Object.assign(new Error('missing binary'), { code: 'ENOENT' })));
127
+ return firstChild;
128
+ })
129
+ .mockImplementationOnce(() => {
130
+ queueMicrotask(() => secondChild.emit('spawn'));
131
+ return secondChild;
132
+ });
133
+ await expect(launchElectronApp('/Applications/Antigravity.app', app, ['--remote-debugging-port=9234'], 'Antigravity')).resolves.toBeUndefined();
134
+ expect(cp.spawn).toHaveBeenNthCalledWith(1, '/Applications/Antigravity.app/Contents/MacOS/Electron', ['--remote-debugging-port=9234'], { detached: true, stdio: 'ignore' });
135
+ expect(cp.spawn).toHaveBeenNthCalledWith(2, '/Applications/Antigravity.app/Contents/MacOS/Antigravity', ['--remote-debugging-port=9234'], { detached: true, stdio: 'ignore' });
136
+ expect(secondChild.unref).toHaveBeenCalledTimes(1);
137
+ });
138
+ });
package/dist/output.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  export interface RenderOptions {
5
5
  fmt?: string;
6
+ /** True when the user explicitly passed -f on the command line */
7
+ fmtExplicit?: boolean;
6
8
  columns?: string[];
7
9
  title?: string;
8
10
  elapsed?: number;
package/dist/output.js CHANGED
@@ -15,7 +15,16 @@ function resolveColumns(rows, opts) {
15
15
  return opts.columns ?? Object.keys(rows[0] ?? {});
16
16
  }
17
17
  export function render(data, opts = {}) {
18
- const fmt = opts.fmt ?? 'table';
18
+ let fmt = opts.fmt ?? 'table';
19
+ // Non-TTY auto-downgrade only when format was NOT explicitly passed by user.
20
+ // Priority: explicit -f (any value) > OUTPUT env var > TTY auto-detect > table
21
+ if (!opts.fmtExplicit) {
22
+ const envFmt = process.env.OUTPUT?.trim().toLowerCase();
23
+ if (envFmt)
24
+ fmt = envFmt;
25
+ else if (fmt === 'table' && !process.stdout.isTTY)
26
+ fmt = 'yaml';
27
+ }
19
28
  if (data === null || data === undefined) {
20
29
  console.log(data);
21
30
  return;
@@ -1,4 +1 @@
1
- /**
2
- * Tests for output.ts: render function format coverage.
3
- */
4
1
  export {};
@@ -1,95 +1,62 @@
1
- /**
2
- * Tests for output.ts: render function format coverage.
3
- */
4
- import { describe, it, expect, vi, afterEach } from 'vitest';
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
5
2
  import { render } from './output.js';
6
- afterEach(() => {
7
- vi.restoreAllMocks();
8
- });
9
- describe('render', () => {
10
- it('renders JSON output', () => {
11
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
12
- render([{ title: 'Hello', rank: 1 }], { fmt: 'json' });
13
- expect(log).toHaveBeenCalledOnce();
14
- const output = log.mock.calls[0]?.[0];
15
- const parsed = JSON.parse(output);
16
- expect(parsed).toEqual([{ title: 'Hello', rank: 1 }]);
17
- });
18
- it('renders Markdown table output', () => {
19
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
20
- render([{ name: 'Alice', score: 100 }], { fmt: 'md', columns: ['name', 'score'] });
21
- const calls = log.mock.calls.map(c => c[0]);
22
- expect(calls[0]).toContain('| name | score |');
23
- expect(calls[1]).toContain('| --- | --- |');
24
- expect(calls[2]).toContain('| Alice | 100 |');
25
- });
26
- it('renders CSV output with proper quoting', () => {
27
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
28
- render([{ name: 'Alice, Bob', value: 'say "hi"' }], { fmt: 'csv' });
29
- const calls = log.mock.calls.map(c => c[0]);
30
- // Header
31
- expect(calls[0]).toBe('name,value');
32
- // Values with commas/quotes are quoted
33
- expect(calls[1]).toContain('"Alice, Bob"');
34
- expect(calls[1]).toContain('"say ""hi"""');
35
- });
36
- it('handles null and undefined data', () => {
37
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
38
- render(null, { fmt: 'json' });
39
- expect(log).toHaveBeenCalledWith(null);
40
- });
41
- it('renders single object as single-row table', () => {
42
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
43
- render({ title: 'Test' }, { fmt: 'json' });
44
- const output = log.mock.calls[0]?.[0];
45
- const parsed = JSON.parse(output);
46
- expect(parsed).toEqual({ title: 'Test' });
47
- });
48
- it('handles empty array gracefully', () => {
49
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
50
- render([], { fmt: 'table' });
51
- // Should show "(no data)" for empty arrays
52
- expect(log).toHaveBeenCalled();
53
- });
54
- it('uses custom columns for CSV', () => {
55
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
56
- render([{ a: 1, b: 2, c: 3 }], { fmt: 'csv', columns: ['a', 'c'] });
57
- const calls = log.mock.calls.map(c => c[0]);
58
- expect(calls[0]).toBe('a,c');
59
- expect(calls[1]).toBe('1,3');
60
- });
61
- it('renders YAML output', () => {
62
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
63
- render([{ title: 'Hello', rank: 1 }], { fmt: 'yaml' });
64
- expect(log).toHaveBeenCalledOnce();
65
- expect(log.mock.calls[0]?.[0]).toContain('- title: Hello');
66
- expect(log.mock.calls[0]?.[0]).toContain('rank: 1');
67
- });
68
- it('renders yml alias as YAML output', () => {
69
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
70
- render({ title: 'Hello' }, { fmt: 'yml' });
71
- expect(log).toHaveBeenCalledOnce();
72
- expect(log.mock.calls[0]?.[0]).toContain('title: Hello');
73
- });
74
- it('handles null values in CSV cells', () => {
75
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
76
- render([{ name: 'test', value: null }], { fmt: 'csv' });
77
- const calls = log.mock.calls.map(c => c[0]);
78
- expect(calls[1]).toBe('test,');
79
- });
80
- it('renders single-field rows in plain mode as the bare value', () => {
81
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
82
- render([{ response: 'Gemini says hi' }], { fmt: 'plain' });
83
- expect(log).toHaveBeenCalledWith('Gemini says hi');
84
- });
85
- it('renders multi-field rows in plain mode as key-value lines', () => {
86
- const log = vi.spyOn(console, 'log').mockImplementation(() => { });
87
- render([{ status: 'ok', file: '~/tmp/a.png', link: 'https://example.com' }], { fmt: 'plain' });
88
- const calls = log.mock.calls.map(c => c[0]);
89
- expect(calls).toEqual([
90
- 'status: ok',
91
- 'file: ~/tmp/a.png',
92
- 'link: https://example.com',
93
- ]);
3
+ describe('output TTY detection', () => {
4
+ const originalIsTTY = process.stdout.isTTY;
5
+ const originalEnv = process.env.OUTPUT;
6
+ let logSpy;
7
+ beforeEach(() => {
8
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
9
+ });
10
+ afterEach(() => {
11
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, writable: true });
12
+ if (originalEnv === undefined)
13
+ delete process.env.OUTPUT;
14
+ else
15
+ process.env.OUTPUT = originalEnv;
16
+ logSpy.mockRestore();
17
+ });
18
+ it('outputs YAML in non-TTY when format is default table', () => {
19
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
20
+ // commanderAdapter always passes fmt:'table' as default this must still trigger downgrade
21
+ render([{ name: 'alice', score: 10 }], { fmt: 'table', columns: ['name', 'score'] });
22
+ const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
23
+ expect(out).toContain('name: alice');
24
+ expect(out).toContain('score: 10');
25
+ });
26
+ it('outputs table in TTY when format is default table', () => {
27
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, writable: true });
28
+ render([{ name: 'alice', score: 10 }], { fmt: 'table', columns: ['name', 'score'] });
29
+ const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
30
+ expect(out).toContain('alice');
31
+ });
32
+ it('respects explicit -f json even in non-TTY', () => {
33
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
34
+ render([{ name: 'alice' }], { fmt: 'json' });
35
+ const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
36
+ expect(JSON.parse(out)).toEqual([{ name: 'alice' }]);
37
+ });
38
+ it('OUTPUT env var overrides default table in non-TTY', () => {
39
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
40
+ process.env.OUTPUT = 'json';
41
+ render([{ name: 'alice' }], { fmt: 'table' });
42
+ const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
43
+ expect(JSON.parse(out)).toEqual([{ name: 'alice' }]);
44
+ });
45
+ it('explicit -f flag takes precedence over OUTPUT env var', () => {
46
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
47
+ process.env.OUTPUT = 'json';
48
+ render([{ name: 'alice' }], { fmt: 'csv', fmtExplicit: true });
49
+ const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
50
+ expect(out).toContain('name');
51
+ expect(out).toContain('alice');
52
+ expect(out).not.toContain('"name"'); // not JSON
53
+ });
54
+ it('explicit -f table overrides non-TTY auto-downgrade', () => {
55
+ Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
56
+ render([{ name: 'alice' }], { fmt: 'table', fmtExplicit: true, columns: ['name'] });
57
+ const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
58
+ // Should be table output, not YAML
59
+ expect(out).not.toContain('name: alice');
60
+ expect(out).toContain('alice');
94
61
  });
95
62
  });
@@ -17,8 +17,6 @@ function createMockPage(overrides = {}) {
17
17
  getFormState: vi.fn().mockResolvedValue({}),
18
18
  wait: vi.fn(),
19
19
  tabs: vi.fn().mockResolvedValue([]),
20
- closeTab: vi.fn(),
21
- newTab: vi.fn(),
22
20
  selectTab: vi.fn(),
23
21
  networkRequests: vi.fn().mockResolvedValue([]),
24
22
  consoleMessages: vi.fn().mockResolvedValue(''),
@@ -29,8 +29,6 @@ function createMockPage(getCookies) {
29
29
  getFormState: vi.fn().mockResolvedValue({}),
30
30
  wait: vi.fn(),
31
31
  tabs: vi.fn().mockResolvedValue([]),
32
- closeTab: vi.fn(),
33
- newTab: vi.fn(),
34
32
  selectTab: vi.fn(),
35
33
  networkRequests: vi.fn().mockResolvedValue([]),
36
34
  consoleMessages: vi.fn().mockResolvedValue([]),
@@ -14,6 +14,7 @@ export interface Arg {
14
14
  type?: string;
15
15
  default?: unknown;
16
16
  required?: boolean;
17
+ valueRequired?: boolean;
17
18
  positional?: boolean;
18
19
  help?: string;
19
20
  choices?: string[];
@@ -40,6 +41,7 @@ export interface CliCommand {
40
41
  source?: string;
41
42
  footerExtra?: (kwargs: CommandArgs) => string | undefined;
42
43
  requiredEnv?: RequiredEnv[];
44
+ validateArgs?: (kwargs: CommandArgs) => void;
43
45
  /** Deprecation note shown in help / execution warnings. */
44
46
  deprecated?: boolean | string;
45
47
  /** Preferred replacement command, if any. */
@@ -9,6 +9,7 @@ export type SerializedArg = {
9
9
  name: string;
10
10
  type: string;
11
11
  required: boolean;
12
+ valueRequired: boolean;
12
13
  positional: boolean;
13
14
  choices: string[];
14
15
  default: unknown;
@@ -11,6 +11,7 @@ export function serializeArg(a) {
11
11
  name: a.name,
12
12
  type: a.type ?? 'string',
13
13
  required: !!a.required,
14
+ valueRequired: !!a.valueRequired,
14
15
  positional: !!a.positional,
15
16
  choices: a.choices ?? [],
16
17
  default: a.default ?? null,
package/dist/types.d.ts CHANGED
@@ -56,8 +56,8 @@ export interface IPage {
56
56
  getFormState(): Promise<any>;
57
57
  wait(options: number | WaitOptions): Promise<void>;
58
58
  tabs(): Promise<any>;
59
- closeTab(index?: number): Promise<void>;
60
- newTab(): Promise<void>;
59
+ closeTab?(index?: number): Promise<void>;
60
+ newTab?(): Promise<void>;
61
61
  selectTab(index: number): Promise<void>;
62
62
  networkRequests(includeStatic?: boolean): Promise<any>;
63
63
  consoleMessages(level?: string): Promise<any>;
@@ -70,11 +70,18 @@ export interface IPage {
70
70
  getInterceptedRequests(): Promise<any[]>;
71
71
  waitForCapture(timeout?: number): Promise<void>;
72
72
  screenshot(options?: ScreenshotOptions): Promise<string>;
73
+ startNetworkCapture?(pattern?: string): Promise<void>;
74
+ readNetworkCapture?(): Promise<unknown[]>;
73
75
  /**
74
76
  * Set local file paths on a file input element via CDP DOM.setFileInputFiles.
75
77
  * Chrome reads the files directly — no base64 encoding or payload size limits.
76
78
  */
77
79
  setFileInput?(files: string[], selector?: string): Promise<void>;
80
+ /**
81
+ * Insert text via native CDP Input.insertText into the currently focused element.
82
+ * Useful for rich editors that ignore synthetic DOM value/text mutations.
83
+ */
84
+ insertText?(text: string): Promise<void>;
78
85
  closeWindow?(): Promise<void>;
79
86
  /** Returns the current page URL, or null if unavailable. */
80
87
  getCurrentUrl?(): Promise<string | null>;