@agentrhq/webcmd 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1290) hide show
  1. package/README.md +278 -20
  2. package/cli-manifest.json +16247 -34913
  3. package/clis/_atlassian/shared.js +1 -1
  4. package/clis/booking/search.js +4 -4
  5. package/clis/chatgpt/ask.js +2 -2
  6. package/clis/chatgpt/utils.js +41 -42
  7. package/clis/chatgpt/utils.test.js +23 -23
  8. package/clis/chatgpt-app/ax.js +5 -5
  9. package/clis/chatgpt-app/ax.test.js +8 -8
  10. package/clis/chatgpt-app/new.js +2 -2
  11. package/clis/codex/sidebar.js +1 -1
  12. package/clis/codex/sidebar.test.js +12 -12
  13. package/clis/coingecko/top.js +3 -3
  14. package/clis/dblp/dblp.test.js +1 -1
  15. package/clis/discord-app/search.js +1 -1
  16. package/clis/discord-app/utils.js +6 -6
  17. package/clis/facebook/__fixtures__/notifications-page.html +6 -6
  18. package/clis/facebook/add-friend.js +2 -2
  19. package/clis/facebook/feed.js +18 -18
  20. package/clis/facebook/friends.js +4 -4
  21. package/clis/facebook/groups.js +3 -3
  22. package/clis/facebook/join-group.js +2 -2
  23. package/clis/facebook/marketplace-inbox.js +3 -3
  24. package/clis/facebook/marketplace-listings.js +1 -1
  25. package/clis/facebook/notifications.js +14 -15
  26. package/clis/facebook/notifications.test.js +28 -29
  27. package/clis/gemini/ask.test.js +12 -12
  28. package/clis/gemini/deep-research-result.js +2 -2
  29. package/clis/gemini/deep-research-result.test.js +2 -2
  30. package/clis/gemini/deep-research.js +2 -2
  31. package/clis/gemini/deep-research.test.js +1 -1
  32. package/clis/gemini/models.js +11 -11
  33. package/clis/gemini/models.test.js +1 -1
  34. package/clis/gemini/reply-state.test.js +89 -89
  35. package/clis/gemini/utils.js +41 -41
  36. package/clis/gemini/utils.test.js +30 -30
  37. package/clis/geogebra/utils.js +1 -1
  38. package/clis/google-scholar/search.js +3 -3
  39. package/clis/grok/delete.js +1 -1
  40. package/clis/grok/export-all.js +2 -2
  41. package/clis/grok/export.js +2 -2
  42. package/clis/grok/pin.js +3 -3
  43. package/clis/grok/utils.js +12 -12
  44. package/clis/grok/utils.test.js +2 -2
  45. package/clis/indeed/indeed.test.js +3 -3
  46. package/clis/indeed/utils.js +1 -1
  47. package/clis/instagram/post.js +19 -19
  48. package/clis/instagram/post.test.js +2 -2
  49. package/clis/instagram/reel.js +6 -6
  50. package/clis/linkedin/connect.js +3 -3
  51. package/clis/linkedin/people-search.js +4 -4
  52. package/clis/linkedin/profile-experience.js +6 -6
  53. package/clis/linkedin/profile-experience.test.js +10 -10
  54. package/clis/linkedin/profile-read.js +1 -1
  55. package/clis/linkedin/safe-send.js +1 -1
  56. package/clis/linkedin/search.js +1 -1
  57. package/clis/linkedin/shared.js +2 -2
  58. package/clis/linkedin/timeline.js +5 -5
  59. package/clis/manus/manus.test.js +2 -2
  60. package/clis/medium/feed.js +3 -3
  61. package/clis/medium/search.js +3 -3
  62. package/clis/medium/user.js +3 -3
  63. package/clis/notebooklm/auth.js +2 -2
  64. package/clis/notebooklm/note-list.test.js +4 -4
  65. package/clis/notebooklm/notes-get.test.js +8 -8
  66. package/clis/notebooklm/source-fulltext.test.js +11 -11
  67. package/clis/notebooklm/utils.js +28 -28
  68. package/clis/notebooklm/utils.test.js +10 -10
  69. package/clis/pixiv/search.test.js +3 -3
  70. package/clis/qoder/quest.js +2 -2
  71. package/clis/reddit/home.js +2 -2
  72. package/clis/reddit/hot.js +1 -1
  73. package/clis/reddit/read.js +2 -2
  74. package/clis/reddit/read.test.js +1 -1
  75. package/clis/reddit/reply.js +2 -2
  76. package/clis/reddit/whoami.js +2 -2
  77. package/clis/reuters/article-detail.js +1 -1
  78. package/clis/reuters/search.js +1 -1
  79. package/clis/slock/attachment-upload.test.js +1 -1
  80. package/clis/slock/task-convert.js +1 -1
  81. package/clis/substack/feed.js +3 -3
  82. package/clis/substack/publication.js +3 -3
  83. package/clis/substack/search.js +4 -4
  84. package/clis/tiktok/comment.js +2 -2
  85. package/clis/tiktok/follow.js +4 -4
  86. package/clis/tiktok/like.js +1 -1
  87. package/clis/tiktok/save.js +1 -1
  88. package/clis/tiktok/unfollow.js +4 -4
  89. package/clis/tiktok/unlike.js +1 -1
  90. package/clis/tiktok/unsave.js +2 -2
  91. package/clis/tiktok/write-refactor.test.js +5 -5
  92. package/clis/twitter/accept.js +3 -3
  93. package/clis/twitter/followers.js +1 -1
  94. package/clis/twitter/list-remove-core.js +2 -2
  95. package/clis/twitter/list-tweets.test.js +3 -3
  96. package/clis/twitter/lists.js +6 -6
  97. package/clis/twitter/lists.test.js +2 -2
  98. package/clis/twitter/reply-dm.js +1 -1
  99. package/clis/twitter/shared.test.js +4 -5
  100. package/clis/twitter/trending.js +1 -1
  101. package/clis/twitter/utils.js +1 -1
  102. package/clis/web/read.js +1 -1
  103. package/clis/web/read.test.js +2 -2
  104. package/clis/yahoo-finance/quote.js +1 -1
  105. package/clis/youtube/history.js +4 -4
  106. package/clis/youtube/transcript-group.js +1 -1
  107. package/clis/youtube/transcript-group.test.js +5 -5
  108. package/clis/youtube/transcript.js +5 -5
  109. package/clis/youtube/video.js +4 -4
  110. package/clis/youtube/video.test.js +6 -6
  111. package/dist/src/adapter-shadow.d.ts +11 -0
  112. package/dist/src/adapter-shadow.js +72 -0
  113. package/dist/src/adapter-shadow.test.js +49 -0
  114. package/dist/src/adapter-source.d.ts +11 -0
  115. package/dist/src/adapter-source.js +24 -0
  116. package/dist/src/adapter-source.test.d.ts +1 -0
  117. package/dist/src/adapter-source.test.js +29 -0
  118. package/dist/src/brand.d.ts +9 -0
  119. package/dist/src/brand.js +9 -0
  120. package/dist/src/brand.test.d.ts +1 -0
  121. package/dist/src/brand.test.js +15 -0
  122. package/dist/src/browser/analyze.d.ts +118 -0
  123. package/dist/src/browser/analyze.js +405 -0
  124. package/dist/src/browser/analyze.test.d.ts +1 -0
  125. package/dist/src/browser/analyze.test.js +209 -0
  126. package/dist/src/browser/article-extract.d.ts +57 -0
  127. package/dist/src/browser/article-extract.e2e.test.d.ts +1 -0
  128. package/dist/src/browser/article-extract.e2e.test.js +105 -0
  129. package/dist/src/browser/article-extract.js +169 -0
  130. package/dist/src/browser/article-extract.test.d.ts +1 -0
  131. package/dist/src/browser/article-extract.test.js +94 -0
  132. package/dist/src/browser/ax-snapshot.d.ts +37 -0
  133. package/dist/src/browser/ax-snapshot.js +217 -0
  134. package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
  135. package/dist/src/browser/ax-snapshot.test.js +91 -0
  136. package/dist/src/browser/base-page.d.ts +151 -0
  137. package/dist/src/browser/base-page.js +1060 -0
  138. package/dist/src/browser/base-page.test.d.ts +1 -0
  139. package/dist/src/browser/base-page.test.js +803 -0
  140. package/dist/src/browser/bridge-readiness.d.ts +14 -0
  141. package/dist/src/browser/bridge-readiness.js +24 -0
  142. package/dist/src/browser/bridge-readiness.test.d.ts +1 -0
  143. package/dist/src/browser/bridge-readiness.test.js +58 -0
  144. package/dist/src/browser/bridge.d.ts +26 -0
  145. package/dist/src/browser/bridge.js +58 -0
  146. package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
  147. package/dist/src/browser/cdp-click-fixture.test.js +87 -0
  148. package/dist/src/browser/cdp.d.ts +46 -0
  149. package/dist/src/browser/cdp.js +536 -0
  150. package/dist/src/browser/cdp.test.d.ts +1 -0
  151. package/dist/src/browser/cdp.test.js +79 -0
  152. package/dist/src/browser/compound.d.ts +59 -0
  153. package/dist/src/browser/compound.js +112 -0
  154. package/dist/src/browser/compound.test.d.ts +1 -0
  155. package/dist/src/browser/compound.test.js +175 -0
  156. package/dist/src/browser/config.d.ts +2 -0
  157. package/dist/src/browser/config.js +14 -0
  158. package/dist/src/browser/daemon-client.d.ts +33 -0
  159. package/dist/src/browser/daemon-client.js +187 -0
  160. package/dist/src/browser/daemon-client.test.d.ts +1 -0
  161. package/dist/src/browser/daemon-client.test.js +411 -0
  162. package/dist/src/browser/daemon-lifecycle.d.ts +37 -0
  163. package/dist/src/browser/daemon-lifecycle.js +147 -0
  164. package/dist/src/browser/daemon-transport.d.ts +54 -0
  165. package/dist/src/browser/daemon-transport.js +68 -0
  166. package/dist/src/browser/daemon-version.d.ts +4 -0
  167. package/dist/src/browser/daemon-version.js +12 -0
  168. package/dist/src/browser/dom-helpers.d.ts +41 -0
  169. package/dist/src/browser/dom-helpers.js +239 -0
  170. package/dist/src/browser/dom-helpers.test.d.ts +1 -0
  171. package/dist/src/browser/dom-helpers.test.js +106 -0
  172. package/dist/src/browser/dom-snapshot.d.ts +93 -0
  173. package/dist/src/browser/dom-snapshot.js +894 -0
  174. package/dist/src/browser/dom-snapshot.test.d.ts +11 -0
  175. package/dist/src/browser/dom-snapshot.test.js +313 -0
  176. package/dist/src/browser/errors.d.ts +38 -0
  177. package/dist/src/browser/errors.js +90 -0
  178. package/dist/src/browser/errors.test.d.ts +1 -0
  179. package/dist/src/browser/errors.test.js +60 -0
  180. package/dist/src/browser/extract.d.ts +69 -0
  181. package/dist/src/browser/extract.js +132 -0
  182. package/dist/src/browser/extract.test.d.ts +1 -0
  183. package/dist/src/browser/extract.test.js +129 -0
  184. package/dist/src/browser/find.d.ts +84 -0
  185. package/dist/src/browser/find.js +398 -0
  186. package/dist/src/browser/find.test.d.ts +1 -0
  187. package/dist/src/browser/find.test.js +180 -0
  188. package/dist/src/browser/html-tree.d.ts +75 -0
  189. package/dist/src/browser/html-tree.js +112 -0
  190. package/dist/src/browser/html-tree.test.d.ts +1 -0
  191. package/dist/src/browser/html-tree.test.js +181 -0
  192. package/dist/src/browser/index.d.ts +14 -0
  193. package/dist/src/browser/index.js +12 -0
  194. package/dist/src/browser/network-cache.d.ts +49 -0
  195. package/dist/src/browser/network-cache.js +78 -0
  196. package/dist/src/browser/network-cache.test.d.ts +1 -0
  197. package/dist/src/browser/network-cache.test.js +75 -0
  198. package/dist/src/browser/network-interceptor.d.ts +11 -0
  199. package/dist/src/browser/network-interceptor.js +11 -0
  200. package/dist/src/browser/network-key.d.ts +22 -0
  201. package/dist/src/browser/network-key.js +66 -0
  202. package/dist/src/browser/network-key.test.d.ts +1 -0
  203. package/dist/src/browser/network-key.test.js +49 -0
  204. package/dist/src/browser/page.d.ts +83 -0
  205. package/dist/src/browser/page.js +453 -0
  206. package/dist/src/browser/page.test.d.ts +1 -0
  207. package/dist/src/browser/page.test.js +406 -0
  208. package/dist/src/browser/profile.d.ts +14 -0
  209. package/dist/src/browser/profile.js +86 -0
  210. package/dist/src/browser/protocol.d.ts +65 -0
  211. package/dist/src/browser/protocol.js +1 -0
  212. package/dist/src/browser/runtime/local-cloak/actions.d.ts +3 -0
  213. package/dist/src/browser/runtime/local-cloak/actions.js +240 -0
  214. package/dist/src/browser/runtime/local-cloak/downloads.d.ts +3 -0
  215. package/dist/src/browser/runtime/local-cloak/downloads.js +28 -0
  216. package/dist/src/browser/runtime/local-cloak/network.d.ts +28 -0
  217. package/dist/src/browser/runtime/local-cloak/network.js +120 -0
  218. package/dist/src/browser/runtime/local-cloak/network.test.d.ts +1 -0
  219. package/dist/src/browser/runtime/local-cloak/network.test.js +124 -0
  220. package/dist/src/browser/runtime/local-cloak/profiles.d.ts +6 -0
  221. package/dist/src/browser/runtime/local-cloak/profiles.js +17 -0
  222. package/dist/src/browser/runtime/local-cloak/profiles.test.d.ts +1 -0
  223. package/dist/src/browser/runtime/local-cloak/profiles.test.js +19 -0
  224. package/dist/src/browser/runtime/local-cloak/provider.d.ts +15 -0
  225. package/dist/src/browser/runtime/local-cloak/provider.js +27 -0
  226. package/dist/src/browser/runtime/local-cloak/provider.test.d.ts +1 -0
  227. package/dist/src/browser/runtime/local-cloak/provider.test.js +244 -0
  228. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +77 -0
  229. package/dist/src/browser/runtime/local-cloak/session-manager.js +280 -0
  230. package/dist/src/browser/runtime/local-cloak/session-manager.test.d.ts +1 -0
  231. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +92 -0
  232. package/dist/src/browser/runtime/provider.d.ts +9 -0
  233. package/dist/src/browser/runtime/provider.js +1 -0
  234. package/dist/src/browser/runtime/provider.test.d.ts +2 -0
  235. package/dist/src/browser/runtime/provider.test.js +49 -0
  236. package/dist/src/browser/shape-filter.d.ts +52 -0
  237. package/dist/src/browser/shape-filter.js +101 -0
  238. package/dist/src/browser/shape-filter.test.d.ts +1 -0
  239. package/dist/src/browser/shape-filter.test.js +101 -0
  240. package/dist/src/browser/shape.d.ts +23 -0
  241. package/dist/src/browser/shape.js +95 -0
  242. package/dist/src/browser/shape.test.d.ts +1 -0
  243. package/dist/src/browser/shape.test.js +82 -0
  244. package/dist/src/browser/stealth.d.ts +11 -0
  245. package/dist/src/browser/stealth.js +359 -0
  246. package/dist/src/browser/stealth.test.d.ts +1 -0
  247. package/dist/src/browser/stealth.test.js +134 -0
  248. package/dist/src/browser/tabs.d.ts +13 -0
  249. package/dist/src/browser/tabs.js +70 -0
  250. package/dist/src/browser/target-errors.d.ts +39 -0
  251. package/dist/src/browser/target-errors.js +45 -0
  252. package/dist/src/browser/target-errors.test.d.ts +1 -0
  253. package/dist/src/browser/target-errors.test.js +94 -0
  254. package/dist/src/browser/target-resolver.d.ts +158 -0
  255. package/dist/src/browser/target-resolver.js +570 -0
  256. package/dist/src/browser/target-resolver.test.d.ts +1 -0
  257. package/dist/src/browser/target-resolver.test.js +118 -0
  258. package/dist/src/browser/utils.d.ts +20 -0
  259. package/dist/src/browser/utils.js +64 -0
  260. package/dist/src/browser/utils.test.d.ts +1 -0
  261. package/dist/src/browser/utils.test.js +29 -0
  262. package/dist/src/browser/verify-fixture.d.ts +65 -0
  263. package/dist/src/browser/verify-fixture.js +318 -0
  264. package/dist/src/browser/verify-fixture.test.d.ts +1 -0
  265. package/dist/src/browser/verify-fixture.test.js +219 -0
  266. package/dist/src/browser/visual-refs.d.ts +11 -0
  267. package/dist/src/browser/visual-refs.js +108 -0
  268. package/dist/src/browser.test.d.ts +1 -0
  269. package/dist/src/browser.test.js +297 -0
  270. package/dist/src/build-manifest.d.ts +103 -0
  271. package/dist/src/build-manifest.js +363 -0
  272. package/dist/src/build-manifest.test.d.ts +1 -0
  273. package/dist/src/build-manifest.test.js +341 -0
  274. package/dist/src/capabilityRouting.d.ts +19 -0
  275. package/dist/src/capabilityRouting.js +56 -0
  276. package/dist/src/capabilityRouting.test.d.ts +1 -0
  277. package/dist/src/capabilityRouting.test.js +63 -0
  278. package/dist/src/cli-argv-preprocess.d.ts +63 -0
  279. package/dist/src/cli-argv-preprocess.js +303 -0
  280. package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
  281. package/dist/src/cli-argv-preprocess.test.js +304 -0
  282. package/dist/src/cli.d.ts +79 -0
  283. package/dist/src/cli.js +3350 -0
  284. package/dist/src/cli.test.d.ts +1 -0
  285. package/dist/src/cli.test.js +3041 -0
  286. package/dist/src/commanderAdapter.d.ts +21 -0
  287. package/dist/src/commanderAdapter.js +208 -0
  288. package/dist/src/commanderAdapter.test.d.ts +1 -0
  289. package/dist/src/commanderAdapter.test.js +352 -0
  290. package/dist/src/commands/auth.d.ts +40 -0
  291. package/dist/src/commands/auth.js +434 -0
  292. package/dist/src/commands/auth.test.d.ts +1 -0
  293. package/dist/src/commands/auth.test.js +252 -0
  294. package/dist/src/commands/daemon.d.ts +9 -0
  295. package/dist/src/commands/daemon.js +91 -0
  296. package/dist/src/commands/daemon.test.d.ts +1 -0
  297. package/dist/src/commands/daemon.test.js +293 -0
  298. package/dist/src/completion-fast.d.ts +22 -0
  299. package/dist/src/completion-fast.js +88 -0
  300. package/dist/src/completion-shared.d.ts +13 -0
  301. package/dist/src/completion-shared.js +61 -0
  302. package/dist/src/completion.d.ts +21 -0
  303. package/dist/src/completion.js +68 -0
  304. package/dist/src/completion.test.d.ts +1 -0
  305. package/dist/src/completion.test.js +24 -0
  306. package/dist/src/constants.d.ts +18 -0
  307. package/dist/src/constants.js +41 -0
  308. package/dist/src/convention-audit.d.ts +50 -0
  309. package/dist/src/convention-audit.js +553 -0
  310. package/dist/src/convention-audit.test.d.ts +1 -0
  311. package/dist/src/convention-audit.test.js +255 -0
  312. package/dist/src/daemon/server.d.ts +13 -0
  313. package/dist/src/daemon/server.js +190 -0
  314. package/dist/src/daemon/server.test.d.ts +1 -0
  315. package/dist/src/daemon/server.test.js +184 -0
  316. package/dist/src/daemon-utils.d.ts +19 -0
  317. package/dist/src/daemon-utils.js +46 -0
  318. package/dist/src/daemon.d.ts +1 -0
  319. package/dist/src/daemon.js +27 -0
  320. package/dist/src/daemon.test.d.ts +1 -0
  321. package/dist/src/daemon.test.js +69 -0
  322. package/dist/src/discovery.d.ts +46 -0
  323. package/dist/src/discovery.js +341 -0
  324. package/dist/src/doctor.d.ts +39 -0
  325. package/dist/src/doctor.js +174 -0
  326. package/dist/src/doctor.test.d.ts +1 -0
  327. package/dist/src/doctor.test.js +309 -0
  328. package/dist/src/download/article-download.d.ts +72 -0
  329. package/dist/src/download/article-download.js +305 -0
  330. package/dist/src/download/article-download.test.d.ts +1 -0
  331. package/dist/src/download/article-download.test.js +235 -0
  332. package/dist/src/download/index.d.ts +71 -0
  333. package/dist/src/download/index.js +363 -0
  334. package/dist/src/download/index.test.d.ts +1 -0
  335. package/dist/src/download/index.test.js +173 -0
  336. package/dist/src/download/media-download.d.ts +49 -0
  337. package/dist/src/download/media-download.js +127 -0
  338. package/dist/src/download/media-download.test.d.ts +1 -0
  339. package/dist/src/download/media-download.test.js +112 -0
  340. package/dist/src/download/progress.d.ts +36 -0
  341. package/dist/src/download/progress.js +120 -0
  342. package/dist/src/download/progress.test.d.ts +1 -0
  343. package/dist/src/download/progress.test.js +36 -0
  344. package/dist/src/electron-apps.d.ts +31 -0
  345. package/dist/src/electron-apps.js +83 -0
  346. package/dist/src/electron-apps.test.d.ts +1 -0
  347. package/dist/src/electron-apps.test.js +67 -0
  348. package/dist/src/engine.test.d.ts +1 -0
  349. package/dist/src/engine.test.js +312 -0
  350. package/dist/src/errors.d.ts +114 -0
  351. package/dist/src/errors.js +174 -0
  352. package/dist/src/errors.test.d.ts +1 -0
  353. package/dist/src/errors.test.js +109 -0
  354. package/dist/src/execution.d.ts +24 -0
  355. package/dist/src/execution.js +511 -0
  356. package/dist/src/execution.test.d.ts +1 -0
  357. package/dist/src/execution.test.js +657 -0
  358. package/dist/src/external-clis.yaml +0 -37
  359. package/dist/src/external.d.ts +49 -0
  360. package/dist/src/external.js +211 -0
  361. package/dist/src/external.test.d.ts +1 -0
  362. package/dist/src/external.test.js +95 -0
  363. package/dist/src/generate-release-notes-cli.test.d.ts +1 -0
  364. package/dist/src/generate-release-notes-cli.test.js +140 -0
  365. package/dist/src/help.d.ts +88 -0
  366. package/dist/src/help.js +556 -0
  367. package/dist/src/help.test.d.ts +1 -0
  368. package/dist/src/help.test.js +58 -0
  369. package/dist/src/hooks.d.ts +46 -0
  370. package/dist/src/hooks.js +58 -0
  371. package/dist/src/hooks.test.d.ts +4 -0
  372. package/dist/src/hooks.test.js +92 -0
  373. package/dist/src/interceptor.d.ts +44 -0
  374. package/dist/src/interceptor.js +183 -0
  375. package/dist/src/interceptor.test.d.ts +4 -0
  376. package/dist/src/interceptor.test.js +81 -0
  377. package/dist/src/launcher.d.ts +41 -0
  378. package/dist/src/launcher.js +226 -0
  379. package/dist/src/launcher.test.d.ts +1 -0
  380. package/dist/src/launcher.test.js +153 -0
  381. package/dist/src/logger.d.ts +26 -0
  382. package/dist/src/logger.js +49 -0
  383. package/dist/src/main.d.ts +5 -0
  384. package/dist/src/main.js +177 -0
  385. package/dist/src/manifest-types.d.ts +43 -0
  386. package/dist/src/manifest-types.js +9 -0
  387. package/dist/src/node-network.d.ts +10 -0
  388. package/dist/src/node-network.js +174 -0
  389. package/dist/src/node-network.test.d.ts +1 -0
  390. package/dist/src/node-network.test.js +55 -0
  391. package/dist/src/observation/artifact.d.ts +16 -0
  392. package/dist/src/observation/artifact.js +260 -0
  393. package/dist/src/observation/artifact.test.d.ts +1 -0
  394. package/dist/src/observation/artifact.test.js +121 -0
  395. package/dist/src/observation/events.d.ts +89 -0
  396. package/dist/src/observation/events.js +1 -0
  397. package/dist/src/observation/index.d.ts +7 -0
  398. package/dist/src/observation/index.js +7 -0
  399. package/dist/src/observation/manager.d.ts +9 -0
  400. package/dist/src/observation/manager.js +27 -0
  401. package/dist/src/observation/manager.test.d.ts +1 -0
  402. package/dist/src/observation/manager.test.js +13 -0
  403. package/dist/src/observation/redaction.d.ts +11 -0
  404. package/dist/src/observation/redaction.js +81 -0
  405. package/dist/src/observation/redaction.test.d.ts +1 -0
  406. package/dist/src/observation/redaction.test.js +32 -0
  407. package/dist/src/observation/retention.d.ts +32 -0
  408. package/dist/src/observation/retention.js +160 -0
  409. package/dist/src/observation/retention.test.d.ts +1 -0
  410. package/dist/src/observation/retention.test.js +118 -0
  411. package/dist/src/observation/ring-buffer.d.ts +22 -0
  412. package/dist/src/observation/ring-buffer.js +45 -0
  413. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  414. package/dist/src/observation/ring-buffer.test.js +22 -0
  415. package/dist/src/observation/session.d.ts +25 -0
  416. package/dist/src/observation/session.js +50 -0
  417. package/dist/src/output.d.ts +14 -0
  418. package/dist/src/output.js +152 -0
  419. package/dist/src/output.test.d.ts +1 -0
  420. package/dist/src/output.test.js +53 -0
  421. package/dist/src/package-exports.test.d.ts +1 -0
  422. package/dist/src/package-exports.test.js +112 -0
  423. package/dist/src/package-paths.d.ts +8 -0
  424. package/dist/src/package-paths.js +41 -0
  425. package/dist/src/pipeline/executor.d.ts +11 -0
  426. package/dist/src/pipeline/executor.js +93 -0
  427. package/dist/src/pipeline/executor.test.d.ts +4 -0
  428. package/dist/src/pipeline/executor.test.js +182 -0
  429. package/dist/src/pipeline/index.d.ts +5 -0
  430. package/dist/src/pipeline/index.js +5 -0
  431. package/dist/src/pipeline/registry.d.ts +28 -0
  432. package/dist/src/pipeline/registry.js +55 -0
  433. package/dist/src/pipeline/steps/browser.d.ts +13 -0
  434. package/dist/src/pipeline/steps/browser.js +86 -0
  435. package/dist/src/pipeline/steps/download.d.ts +18 -0
  436. package/dist/src/pipeline/steps/download.js +252 -0
  437. package/dist/src/pipeline/steps/download.test.d.ts +1 -0
  438. package/dist/src/pipeline/steps/download.test.js +102 -0
  439. package/dist/src/pipeline/steps/fetch.d.ts +5 -0
  440. package/dist/src/pipeline/steps/fetch.js +122 -0
  441. package/dist/src/pipeline/steps/fetch.test.d.ts +1 -0
  442. package/dist/src/pipeline/steps/fetch.test.js +117 -0
  443. package/dist/src/pipeline/steps/intercept.d.ts +5 -0
  444. package/dist/src/pipeline/steps/intercept.js +50 -0
  445. package/dist/src/pipeline/steps/tap.d.ts +12 -0
  446. package/dist/src/pipeline/steps/tap.js +93 -0
  447. package/dist/src/pipeline/steps/transform.d.ts +9 -0
  448. package/dist/src/pipeline/steps/transform.js +70 -0
  449. package/dist/src/pipeline/template.d.ts +17 -0
  450. package/dist/src/pipeline/template.js +344 -0
  451. package/dist/src/pipeline/template.test.d.ts +4 -0
  452. package/dist/src/pipeline/template.test.js +197 -0
  453. package/dist/src/pipeline/transform.test.d.ts +4 -0
  454. package/dist/src/pipeline/transform.test.js +133 -0
  455. package/dist/src/plugin-manifest.d.ts +70 -0
  456. package/dist/src/plugin-manifest.js +160 -0
  457. package/dist/src/plugin-manifest.test.d.ts +4 -0
  458. package/dist/src/plugin-manifest.test.js +179 -0
  459. package/dist/src/plugin-scaffold.d.ts +28 -0
  460. package/dist/src/plugin-scaffold.js +143 -0
  461. package/dist/src/plugin-scaffold.test.d.ts +4 -0
  462. package/dist/src/plugin-scaffold.test.js +83 -0
  463. package/dist/src/plugin.d.ts +146 -0
  464. package/dist/src/plugin.js +1303 -0
  465. package/dist/src/plugin.test.d.ts +4 -0
  466. package/dist/src/plugin.test.js +1363 -0
  467. package/dist/src/registry-api.d.ts +13 -0
  468. package/dist/src/registry-api.js +10 -0
  469. package/dist/src/registry.d.ts +123 -0
  470. package/dist/src/registry.js +125 -0
  471. package/dist/src/registry.test.d.ts +4 -0
  472. package/dist/src/registry.test.js +209 -0
  473. package/dist/src/release-notes.d.ts +38 -0
  474. package/dist/src/release-notes.js +99 -0
  475. package/dist/src/release-notes.test.d.ts +1 -0
  476. package/dist/src/release-notes.test.js +61 -0
  477. package/dist/src/runtime-copy.test.d.ts +1 -0
  478. package/dist/src/runtime-copy.test.js +29 -0
  479. package/dist/src/runtime-detect.d.ts +31 -0
  480. package/dist/src/runtime-detect.js +47 -0
  481. package/dist/src/runtime-detect.test.d.ts +1 -0
  482. package/dist/src/runtime-detect.test.js +39 -0
  483. package/dist/src/runtime-identity.test.d.ts +1 -0
  484. package/dist/src/runtime-identity.test.js +18 -0
  485. package/dist/src/runtime.d.ts +47 -0
  486. package/dist/src/runtime.js +54 -0
  487. package/dist/src/scripts/framework.d.ts +7 -0
  488. package/dist/src/scripts/framework.js +25 -0
  489. package/dist/src/scripts/interact.d.ts +4 -0
  490. package/dist/src/scripts/interact.js +20 -0
  491. package/dist/src/scripts/store.d.ts +13 -0
  492. package/dist/src/scripts/store.js +48 -0
  493. package/dist/src/serialization.d.ts +42 -0
  494. package/dist/src/serialization.js +103 -0
  495. package/dist/src/serialization.test.d.ts +1 -0
  496. package/dist/src/serialization.test.js +78 -0
  497. package/dist/src/skills.d.ts +14 -0
  498. package/dist/src/skills.js +127 -0
  499. package/dist/src/skills.test.d.ts +1 -0
  500. package/dist/src/skills.test.js +71 -0
  501. package/dist/src/snapshotFormatter.d.ts +11 -0
  502. package/dist/src/snapshotFormatter.js +336 -0
  503. package/dist/src/snapshotFormatter.test.d.ts +7 -0
  504. package/dist/src/snapshotFormatter.test.js +521 -0
  505. package/dist/src/tui.d.ts +28 -0
  506. package/dist/src/tui.js +179 -0
  507. package/dist/src/tui.test.d.ts +1 -0
  508. package/dist/src/tui.test.js +19 -0
  509. package/dist/src/types.d.ts +236 -0
  510. package/dist/src/types.js +7 -0
  511. package/dist/src/update-check.d.ts +66 -0
  512. package/dist/src/update-check.js +206 -0
  513. package/dist/src/update-check.test.d.ts +1 -0
  514. package/dist/src/update-check.test.js +117 -0
  515. package/dist/src/utils.d.ts +57 -0
  516. package/dist/src/utils.js +152 -0
  517. package/dist/src/utils.test.d.ts +1 -0
  518. package/dist/src/utils.test.js +155 -0
  519. package/dist/src/validate.d.ts +21 -0
  520. package/dist/src/validate.js +130 -0
  521. package/dist/src/validate.test.d.ts +9 -0
  522. package/dist/src/validate.test.js +90 -0
  523. package/dist/src/verify.d.ts +26 -0
  524. package/dist/src/verify.js +67 -0
  525. package/dist/src/version.d.ts +4 -0
  526. package/dist/src/version.js +22 -0
  527. package/package.json +7 -3
  528. package/scripts/check-package-bin.mjs +95 -0
  529. package/scripts/generate-release-notes.ts +234 -0
  530. package/scripts/postinstall.js +1 -1
  531. package/scripts/silent-column-drop-baseline.json +0 -498
  532. package/scripts/typed-error-lint-baseline.json +0 -464
  533. package/skills/webcmd-adapter-author/SKILL.md +216 -213
  534. package/skills/webcmd-adapter-author/references/adapter-template.md +206 -399
  535. package/skills/webcmd-adapter-author/references/api-discovery.md +111 -220
  536. package/skills/webcmd-adapter-author/references/coverage-matrix.md +34 -68
  537. package/skills/webcmd-adapter-author/references/field-conventions.md +84 -167
  538. package/skills/webcmd-adapter-author/references/field-decode-playbook.md +60 -146
  539. package/skills/webcmd-adapter-author/references/jsdom-fixture-pattern.md +111 -178
  540. package/skills/webcmd-adapter-author/references/output-design.md +47 -125
  541. package/skills/webcmd-adapter-author/references/site-memory.md +116 -166
  542. package/skills/webcmd-adapter-author/references/site-recon.md +74 -78
  543. package/skills/webcmd-adapter-author/references/strategy-selection.md +80 -124
  544. package/skills/webcmd-adapter-author/references/success-rate-pitfalls.md +80 -105
  545. package/skills/webcmd-adapter-author/references/typed-errors.md +87 -178
  546. package/skills/webcmd-autofix/SKILL.md +136 -143
  547. package/skills/webcmd-sitemap-author/SKILL.md +58 -67
  548. package/skills/webcmd-sitemap-author/references/sitemap-schema.md +354 -494
  549. package/skills/webcmd-usage/SKILL.md +106 -97
  550. package/clis/12306/auth.js +0 -59
  551. package/clis/12306/me.js +0 -73
  552. package/clis/12306/orders.js +0 -96
  553. package/clis/12306/passengers.js +0 -90
  554. package/clis/12306/price.js +0 -166
  555. package/clis/12306/stations.js +0 -66
  556. package/clis/12306/train.js +0 -91
  557. package/clis/12306/trains.js +0 -166
  558. package/clis/12306/utils.js +0 -272
  559. package/clis/12306/utils.test.js +0 -424
  560. package/clis/1688/assets.js +0 -205
  561. package/clis/1688/assets.test.js +0 -39
  562. package/clis/1688/auth.js +0 -46
  563. package/clis/1688/download.js +0 -77
  564. package/clis/1688/download.test.js +0 -31
  565. package/clis/1688/item.js +0 -188
  566. package/clis/1688/item.test.js +0 -67
  567. package/clis/1688/search.js +0 -310
  568. package/clis/1688/search.test.js +0 -75
  569. package/clis/1688/shared.js +0 -557
  570. package/clis/1688/shared.test.js +0 -57
  571. package/clis/1688/store.js +0 -227
  572. package/clis/1688/store.test.js +0 -62
  573. package/clis/1point3acres/auth.js +0 -52
  574. package/clis/1point3acres/digest.js +0 -35
  575. package/clis/1point3acres/forum.js +0 -51
  576. package/clis/1point3acres/forums.js +0 -44
  577. package/clis/1point3acres/hot.js +0 -35
  578. package/clis/1point3acres/latest.js +0 -35
  579. package/clis/1point3acres/notifications.js +0 -64
  580. package/clis/1point3acres/search.js +0 -71
  581. package/clis/1point3acres/thread.js +0 -117
  582. package/clis/1point3acres/user.js +0 -77
  583. package/clis/1point3acres/utils.js +0 -247
  584. package/clis/36kr/article.js +0 -66
  585. package/clis/36kr/article.test.js +0 -46
  586. package/clis/36kr/hot.js +0 -87
  587. package/clis/36kr/hot.test.js +0 -15
  588. package/clis/36kr/news.js +0 -52
  589. package/clis/36kr/news.test.js +0 -85
  590. package/clis/36kr/search.js +0 -79
  591. package/clis/51job/company.js +0 -126
  592. package/clis/51job/detail.js +0 -109
  593. package/clis/51job/hot.js +0 -56
  594. package/clis/51job/search.js +0 -80
  595. package/clis/51job/utils.js +0 -302
  596. package/clis/51job/utils.test.js +0 -69
  597. package/clis/aibase/news.js +0 -110
  598. package/clis/aibase/news.test.js +0 -59
  599. package/clis/autohome/__fixtures__/catalog.html +0 -11
  600. package/clis/autohome/__fixtures__/koubei.json +0 -116
  601. package/clis/autohome/autohome.test.js +0 -115
  602. package/clis/autohome/brand.js +0 -108
  603. package/clis/autohome/score.js +0 -103
  604. package/clis/autohome/utils.js +0 -157
  605. package/clis/baidu-scholar/search.js +0 -87
  606. package/clis/baidu-scholar/search.test.js +0 -23
  607. package/clis/bilibili/auth.js +0 -36
  608. package/clis/bilibili/comment.js +0 -107
  609. package/clis/bilibili/comment.test.js +0 -153
  610. package/clis/bilibili/comments.js +0 -136
  611. package/clis/bilibili/comments.test.js +0 -142
  612. package/clis/bilibili/download.js +0 -178
  613. package/clis/bilibili/download.test.js +0 -173
  614. package/clis/bilibili/dynamic.js +0 -34
  615. package/clis/bilibili/dynamic.test.js +0 -68
  616. package/clis/bilibili/favorite.js +0 -47
  617. package/clis/bilibili/feed.js +0 -220
  618. package/clis/bilibili/follow.js +0 -140
  619. package/clis/bilibili/follow.test.js +0 -203
  620. package/clis/bilibili/following.js +0 -44
  621. package/clis/bilibili/history.js +0 -45
  622. package/clis/bilibili/hot.js +0 -40
  623. package/clis/bilibili/hot.test.js +0 -17
  624. package/clis/bilibili/me.js +0 -13
  625. package/clis/bilibili/ranking.js +0 -25
  626. package/clis/bilibili/search.js +0 -24
  627. package/clis/bilibili/subtitle.js +0 -141
  628. package/clis/bilibili/subtitle.test.js +0 -240
  629. package/clis/bilibili/summary.js +0 -167
  630. package/clis/bilibili/summary.test.js +0 -210
  631. package/clis/bilibili/unfollow.js +0 -121
  632. package/clis/bilibili/user-videos.js +0 -39
  633. package/clis/bilibili/utils.js +0 -273
  634. package/clis/bilibili/utils.test.js +0 -97
  635. package/clis/bilibili/video.js +0 -155
  636. package/clis/bilibili/video.test.js +0 -308
  637. package/clis/boss/auth.js +0 -47
  638. package/clis/boss/batchgreet.js +0 -61
  639. package/clis/boss/chatlist.js +0 -116
  640. package/clis/boss/chatlist.test.js +0 -211
  641. package/clis/boss/chatmsg.js +0 -117
  642. package/clis/boss/chatmsg.test.js +0 -230
  643. package/clis/boss/detail.js +0 -63
  644. package/clis/boss/exchange.js +0 -46
  645. package/clis/boss/greet.js +0 -49
  646. package/clis/boss/invite.js +0 -64
  647. package/clis/boss/joblist.js +0 -31
  648. package/clis/boss/mark.js +0 -70
  649. package/clis/boss/recommend.js +0 -45
  650. package/clis/boss/resume.js +0 -150
  651. package/clis/boss/search.js +0 -197
  652. package/clis/boss/search.test.js +0 -78
  653. package/clis/boss/send.js +0 -44
  654. package/clis/boss/stats.js +0 -66
  655. package/clis/boss/utils.js +0 -468
  656. package/clis/boss/utils.test.js +0 -34
  657. package/clis/chaoxing/assignments.js +0 -76
  658. package/clis/chaoxing/auth.js +0 -54
  659. package/clis/chaoxing/exams.js +0 -75
  660. package/clis/chaoxing/utils.js +0 -223
  661. package/clis/chaoxing/utils.test.js +0 -45
  662. package/clis/cnki/search.js +0 -61
  663. package/clis/cnki/search.test.js +0 -18
  664. package/clis/ctrip/auth.js +0 -50
  665. package/clis/ctrip/ctrip.test.js +0 -719
  666. package/clis/ctrip/flight.js +0 -136
  667. package/clis/ctrip/hotel-search.js +0 -132
  668. package/clis/ctrip/hotel-suggest.js +0 -45
  669. package/clis/ctrip/search.js +0 -41
  670. package/clis/ctrip/utils.js +0 -473
  671. package/clis/deepseek/ask.js +0 -158
  672. package/clis/deepseek/ask.test.js +0 -399
  673. package/clis/deepseek/auth.js +0 -54
  674. package/clis/deepseek/detail.js +0 -46
  675. package/clis/deepseek/detail.test.js +0 -81
  676. package/clis/deepseek/history.js +0 -27
  677. package/clis/deepseek/new.js +0 -33
  678. package/clis/deepseek/read.js +0 -25
  679. package/clis/deepseek/send.js +0 -140
  680. package/clis/deepseek/send.test.js +0 -107
  681. package/clis/deepseek/status.js +0 -26
  682. package/clis/deepseek/utils.js +0 -487
  683. package/clis/deepseek/utils.test.js +0 -370
  684. package/clis/dianping/__fixtures__/search.html +0 -168
  685. package/clis/dianping/__fixtures__/shop.html +0 -6
  686. package/clis/dianping/auth.js +0 -49
  687. package/clis/dianping/cityResolver.js +0 -185
  688. package/clis/dianping/dianping.test.js +0 -578
  689. package/clis/dianping/search.js +0 -157
  690. package/clis/dianping/shop.js +0 -173
  691. package/clis/dianping/utils.js +0 -157
  692. package/clis/dongchedi/__fixtures__/search.json +0 -60
  693. package/clis/dongchedi/__fixtures__/series-detail.json +0 -911
  694. package/clis/dongchedi/__fixtures__/series-score.json +0 -1396
  695. package/clis/dongchedi/dongchedi.test.js +0 -191
  696. package/clis/dongchedi/koubei.js +0 -85
  697. package/clis/dongchedi/models.js +0 -91
  698. package/clis/dongchedi/score.js +0 -83
  699. package/clis/dongchedi/search.js +0 -83
  700. package/clis/dongchedi/series.js +0 -87
  701. package/clis/dongchedi/specs.js +0 -115
  702. package/clis/dongchedi/utils.js +0 -180
  703. package/clis/douban/auth.js +0 -50
  704. package/clis/douban/book-hot.js +0 -15
  705. package/clis/douban/download.js +0 -68
  706. package/clis/douban/download.test.js +0 -170
  707. package/clis/douban/marks.js +0 -116
  708. package/clis/douban/movie-hot.js +0 -15
  709. package/clis/douban/movie-hot.test.js +0 -14
  710. package/clis/douban/photos.js +0 -35
  711. package/clis/douban/reviews.js +0 -107
  712. package/clis/douban/search.js +0 -18
  713. package/clis/douban/search.test.js +0 -11
  714. package/clis/douban/subject.js +0 -46
  715. package/clis/douban/subject.test.js +0 -11
  716. package/clis/douban/top250.js +0 -68
  717. package/clis/douban/utils.js +0 -657
  718. package/clis/douban/utils.test.js +0 -488
  719. package/clis/doubao/ask.js +0 -40
  720. package/clis/doubao/auth.js +0 -66
  721. package/clis/doubao/detail.js +0 -35
  722. package/clis/doubao/detail.test.js +0 -42
  723. package/clis/doubao/history.js +0 -30
  724. package/clis/doubao/history.test.js +0 -37
  725. package/clis/doubao/meeting-summary.js +0 -41
  726. package/clis/doubao/meeting-transcript.js +0 -38
  727. package/clis/doubao/new.js +0 -22
  728. package/clis/doubao/read.js +0 -21
  729. package/clis/doubao/send.js +0 -24
  730. package/clis/doubao/status.js +0 -26
  731. package/clis/doubao/utils.js +0 -1156
  732. package/clis/doubao/utils.test.js +0 -396
  733. package/clis/doubao-app/ask.js +0 -54
  734. package/clis/doubao-app/dump.js +0 -25
  735. package/clis/doubao-app/new.js +0 -21
  736. package/clis/doubao-app/read.js +0 -19
  737. package/clis/doubao-app/screenshot.js +0 -19
  738. package/clis/doubao-app/send.js +0 -28
  739. package/clis/doubao-app/status.js +0 -17
  740. package/clis/doubao-app/utils.js +0 -110
  741. package/clis/douyin/_shared/browser-fetch.js +0 -66
  742. package/clis/douyin/_shared/browser-fetch.test.js +0 -64
  743. package/clis/douyin/_shared/creation-id.js +0 -5
  744. package/clis/douyin/_shared/creation-id.test.js +0 -22
  745. package/clis/douyin/_shared/evaluate-result.js +0 -16
  746. package/clis/douyin/_shared/imagex-upload.js +0 -53
  747. package/clis/douyin/_shared/imagex-upload.test.js +0 -87
  748. package/clis/douyin/_shared/public-api.js +0 -29
  749. package/clis/douyin/_shared/sts2.js +0 -21
  750. package/clis/douyin/_shared/sts2.test.js +0 -27
  751. package/clis/douyin/_shared/text-extra.js +0 -15
  752. package/clis/douyin/_shared/text-extra.test.js +0 -37
  753. package/clis/douyin/_shared/timing.js +0 -22
  754. package/clis/douyin/_shared/timing.test.js +0 -28
  755. package/clis/douyin/_shared/tos-upload-short-read.test.js +0 -83
  756. package/clis/douyin/_shared/tos-upload.js +0 -331
  757. package/clis/douyin/_shared/tos-upload.test.js +0 -229
  758. package/clis/douyin/_shared/transcode.js +0 -45
  759. package/clis/douyin/_shared/transcode.test.js +0 -91
  760. package/clis/douyin/_shared/vod-upload.js +0 -212
  761. package/clis/douyin/_shared/vod-upload.test.js +0 -38
  762. package/clis/douyin/activities.js +0 -23
  763. package/clis/douyin/activities.test.js +0 -55
  764. package/clis/douyin/auth.js +0 -39
  765. package/clis/douyin/collections.js +0 -23
  766. package/clis/douyin/collections.test.js +0 -45
  767. package/clis/douyin/delete.js +0 -152
  768. package/clis/douyin/delete.test.js +0 -100
  769. package/clis/douyin/draft.js +0 -355
  770. package/clis/douyin/draft.test.js +0 -335
  771. package/clis/douyin/drafts.js +0 -24
  772. package/clis/douyin/drafts.test.js +0 -11
  773. package/clis/douyin/hashtag.js +0 -114
  774. package/clis/douyin/hashtag.test.js +0 -171
  775. package/clis/douyin/location.js +0 -25
  776. package/clis/douyin/location.test.js +0 -23
  777. package/clis/douyin/profile.js +0 -29
  778. package/clis/douyin/profile.test.js +0 -46
  779. package/clis/douyin/publish-upload-id.test.js +0 -206
  780. package/clis/douyin/publish.js +0 -341
  781. package/clis/douyin/publish.test.js +0 -38
  782. package/clis/douyin/search.js +0 -308
  783. package/clis/douyin/search.test.js +0 -307
  784. package/clis/douyin/stats.js +0 -28
  785. package/clis/douyin/stats.test.js +0 -22
  786. package/clis/douyin/update.js +0 -32
  787. package/clis/douyin/update.test.js +0 -11
  788. package/clis/douyin/user-videos.js +0 -82
  789. package/clis/douyin/user-videos.test.js +0 -151
  790. package/clis/douyin/videos.js +0 -52
  791. package/clis/douyin/videos.test.js +0 -54
  792. package/clis/eastmoney/_secid.js +0 -78
  793. package/clis/eastmoney/announcement.js +0 -53
  794. package/clis/eastmoney/convertible.js +0 -74
  795. package/clis/eastmoney/etf.js +0 -66
  796. package/clis/eastmoney/holders.js +0 -79
  797. package/clis/eastmoney/hot-rank.js +0 -51
  798. package/clis/eastmoney/hot-rank.test.js +0 -59
  799. package/clis/eastmoney/index-board.js +0 -97
  800. package/clis/eastmoney/kline.js +0 -88
  801. package/clis/eastmoney/kuaixun.js +0 -55
  802. package/clis/eastmoney/longhu.js +0 -68
  803. package/clis/eastmoney/money-flow.js +0 -79
  804. package/clis/eastmoney/northbound.js +0 -58
  805. package/clis/eastmoney/quote.js +0 -108
  806. package/clis/eastmoney/rank.js +0 -95
  807. package/clis/eastmoney/sectors.js +0 -77
  808. package/clis/flomo/auth.js +0 -55
  809. package/clis/flomo/memos.js +0 -228
  810. package/clis/flomo/memos.test.js +0 -144
  811. package/clis/gitee/auth.js +0 -41
  812. package/clis/gitee/index.js +0 -3
  813. package/clis/gitee/search.js +0 -137
  814. package/clis/gitee/search.test.js +0 -65
  815. package/clis/gitee/trending.js +0 -568
  816. package/clis/gitee/user.js +0 -200
  817. package/clis/gitee/user.test.js +0 -63
  818. package/clis/gov-law/commands.test.js +0 -39
  819. package/clis/gov-law/recent.js +0 -22
  820. package/clis/gov-law/search.js +0 -41
  821. package/clis/gov-law/shared.js +0 -51
  822. package/clis/gov-policy/__fixtures__/recent.html +0 -16
  823. package/clis/gov-policy/__fixtures__/search.html +0 -41
  824. package/clis/gov-policy/gov-policy.test.js +0 -224
  825. package/clis/gov-policy/recent.js +0 -90
  826. package/clis/gov-policy/search.js +0 -91
  827. package/clis/gov-policy/utils.js +0 -54
  828. package/clis/guazi/__fixtures__/detail.html +0 -7
  829. package/clis/guazi/__fixtures__/list.html +0 -5
  830. package/clis/guazi/browse.js +0 -95
  831. package/clis/guazi/car.js +0 -110
  832. package/clis/guazi/guazi.test.js +0 -126
  833. package/clis/guazi/utils.js +0 -133
  834. package/clis/huodongxing/events.js +0 -322
  835. package/clis/huodongxing/events.test.js +0 -296
  836. package/clis/hupu/__fixtures__/hot-home.html +0 -64
  837. package/clis/hupu/auth.js +0 -47
  838. package/clis/hupu/detail.js +0 -72
  839. package/clis/hupu/hot.js +0 -163
  840. package/clis/hupu/hot.test.js +0 -224
  841. package/clis/hupu/like.js +0 -76
  842. package/clis/hupu/mentions.js +0 -160
  843. package/clis/hupu/reply.js +0 -72
  844. package/clis/hupu/search.js +0 -60
  845. package/clis/hupu/unlike.js +0 -76
  846. package/clis/hupu/utils.js +0 -319
  847. package/clis/jd/add-cart.js +0 -72
  848. package/clis/jd/auth.js +0 -46
  849. package/clis/jd/cart.js +0 -80
  850. package/clis/jd/commands.test.js +0 -41
  851. package/clis/jd/detail.js +0 -63
  852. package/clis/jd/item.js +0 -730
  853. package/clis/jd/item.test.js +0 -354
  854. package/clis/jd/item.test.ts +0 -517
  855. package/clis/jd/reviews.js +0 -55
  856. package/clis/jd/search.js +0 -66
  857. package/clis/jianyu/auth.js +0 -62
  858. package/clis/jianyu/detail.js +0 -21
  859. package/clis/jianyu/search.js +0 -634
  860. package/clis/jianyu/search.test.js +0 -153
  861. package/clis/jianyu/shared/china-bid-search.js +0 -165
  862. package/clis/jianyu/shared/procurement-contract.js +0 -324
  863. package/clis/jianyu/shared/procurement-contract.test.js +0 -72
  864. package/clis/jianyu/shared/procurement-detail.js +0 -107
  865. package/clis/jianyu/shared/procurement-detail.test.js +0 -84
  866. package/clis/jike/auth.js +0 -48
  867. package/clis/jike/comment.js +0 -107
  868. package/clis/jike/create.js +0 -106
  869. package/clis/jike/feed.js +0 -68
  870. package/clis/jike/like.js +0 -61
  871. package/clis/jike/notifications.js +0 -169
  872. package/clis/jike/post.js +0 -72
  873. package/clis/jike/read.test.js +0 -86
  874. package/clis/jike/repost.js +0 -103
  875. package/clis/jike/search.js +0 -68
  876. package/clis/jike/topic.js +0 -65
  877. package/clis/jike/user.js +0 -65
  878. package/clis/jike/utils.js +0 -25
  879. package/clis/jimeng/auth.js +0 -46
  880. package/clis/jimeng/generate.js +0 -84
  881. package/clis/jimeng/history.js +0 -48
  882. package/clis/jimeng/new.js +0 -44
  883. package/clis/jimeng/workspaces.js +0 -42
  884. package/clis/juejin/hot.js +0 -37
  885. package/clis/juejin/juejin.test.js +0 -241
  886. package/clis/juejin/recommend.js +0 -61
  887. package/clis/juejin/utils.js +0 -199
  888. package/clis/ke/auth.js +0 -46
  889. package/clis/ke/chengjiao.js +0 -78
  890. package/clis/ke/ershoufang.js +0 -101
  891. package/clis/ke/utils.js +0 -104
  892. package/clis/ke/xiaoqu.js +0 -78
  893. package/clis/ke/zufang.js +0 -95
  894. package/clis/kimi/_utils.js +0 -110
  895. package/clis/kimi/audit-extras.js +0 -268
  896. package/clis/kimi/auth.js +0 -57
  897. package/clis/kimi/chat.js +0 -470
  898. package/clis/kimi/kimi.test.js +0 -187
  899. package/clis/kimi/storage.js +0 -169
  900. package/clis/kimi/ui.js +0 -314
  901. package/clis/kimi/usage.js +0 -144
  902. package/clis/linux-do/auth.js +0 -55
  903. package/clis/linux-do/categories.js +0 -50
  904. package/clis/linux-do/feed.js +0 -395
  905. package/clis/linux-do/feed.test.js +0 -153
  906. package/clis/linux-do/search.js +0 -29
  907. package/clis/linux-do/tags.js +0 -28
  908. package/clis/linux-do/topic-content.js +0 -155
  909. package/clis/linux-do/topic-content.test.js +0 -59
  910. package/clis/linux-do/topic.js +0 -53
  911. package/clis/linux-do/user-posts.js +0 -57
  912. package/clis/linux-do/user-topics.js +0 -37
  913. package/clis/maimai/auth.js +0 -47
  914. package/clis/maimai/search-talents.js +0 -192
  915. package/clis/maimai/search-talents.test.js +0 -29
  916. package/clis/mubu/doc.js +0 -41
  917. package/clis/mubu/docs.js +0 -44
  918. package/clis/mubu/notes.js +0 -245
  919. package/clis/mubu/recent.js +0 -28
  920. package/clis/mubu/search.js +0 -63
  921. package/clis/mubu/utils.js +0 -304
  922. package/clis/nowcoder/auth.js +0 -65
  923. package/clis/nowcoder/companies.js +0 -24
  924. package/clis/nowcoder/creators.js +0 -28
  925. package/clis/nowcoder/detail.js +0 -62
  926. package/clis/nowcoder/experience.js +0 -37
  927. package/clis/nowcoder/hot.js +0 -25
  928. package/clis/nowcoder/jobs.js +0 -22
  929. package/clis/nowcoder/notifications.js +0 -30
  930. package/clis/nowcoder/papers.js +0 -41
  931. package/clis/nowcoder/practice.js +0 -38
  932. package/clis/nowcoder/recommend.js +0 -31
  933. package/clis/nowcoder/referral.js +0 -40
  934. package/clis/nowcoder/salary.js +0 -41
  935. package/clis/nowcoder/search.js +0 -50
  936. package/clis/nowcoder/suggest.js +0 -34
  937. package/clis/nowcoder/topics.js +0 -28
  938. package/clis/nowcoder/trending.js +0 -26
  939. package/clis/ones/common.js +0 -144
  940. package/clis/ones/enrich-tasks.js +0 -37
  941. package/clis/ones/login.js +0 -81
  942. package/clis/ones/logout.js +0 -18
  943. package/clis/ones/me.js +0 -31
  944. package/clis/ones/my-tasks.js +0 -121
  945. package/clis/ones/resolve-labels.js +0 -64
  946. package/clis/ones/task-helpers.js +0 -212
  947. package/clis/ones/task-helpers.test.js +0 -12
  948. package/clis/ones/task.js +0 -67
  949. package/clis/ones/tasks.js +0 -80
  950. package/clis/ones/token-info.js +0 -43
  951. package/clis/ones/worklog.js +0 -268
  952. package/clis/ones/worklog.test.js +0 -20
  953. package/clis/powerchina/auth.js +0 -52
  954. package/clis/powerchina/search.js +0 -251
  955. package/clis/powerchina/search.test.js +0 -93
  956. package/clis/quark/auth.js +0 -47
  957. package/clis/quark/ls.js +0 -64
  958. package/clis/quark/mkdir.js +0 -37
  959. package/clis/quark/mv.js +0 -54
  960. package/clis/quark/rename.js +0 -27
  961. package/clis/quark/rm.js +0 -25
  962. package/clis/quark/save.js +0 -81
  963. package/clis/quark/share-tree.js +0 -46
  964. package/clis/quark/utils.js +0 -146
  965. package/clis/quark/utils.test.js +0 -58
  966. package/clis/qwen/ask.js +0 -104
  967. package/clis/qwen/ask.test.js +0 -36
  968. package/clis/qwen/auth.js +0 -56
  969. package/clis/qwen/detail.js +0 -62
  970. package/clis/qwen/history.js +0 -61
  971. package/clis/qwen/image.js +0 -179
  972. package/clis/qwen/new.js +0 -23
  973. package/clis/qwen/read.js +0 -41
  974. package/clis/qwen/send.js +0 -55
  975. package/clis/qwen/status.js +0 -37
  976. package/clis/qwen/utils.js +0 -461
  977. package/clis/qwen/utils.test.js +0 -128
  978. package/clis/rednote/auth.js +0 -52
  979. package/clis/rednote/comments.js +0 -85
  980. package/clis/rednote/download.js +0 -59
  981. package/clis/rednote/feed.js +0 -27
  982. package/clis/rednote/navigation.test.js +0 -26
  983. package/clis/rednote/note.js +0 -68
  984. package/clis/rednote/notifications.js +0 -139
  985. package/clis/rednote/rednote.test.js +0 -240
  986. package/clis/rednote/search.js +0 -107
  987. package/clis/rednote/user.js +0 -55
  988. package/clis/sinablog/article.js +0 -15
  989. package/clis/sinablog/hot.js +0 -15
  990. package/clis/sinablog/search.js +0 -52
  991. package/clis/sinablog/user.js +0 -16
  992. package/clis/sinablog/utils.js +0 -186
  993. package/clis/sinafinance/news.js +0 -62
  994. package/clis/sinafinance/rolling-news.js +0 -41
  995. package/clis/sinafinance/stock-rank.js +0 -66
  996. package/clis/sinafinance/stock.js +0 -121
  997. package/clis/sinafinance/stock.test.js +0 -59
  998. package/clis/smzdm/search.js +0 -149
  999. package/clis/smzdm/search.test.js +0 -114
  1000. package/clis/taobao/add-cart.js +0 -150
  1001. package/clis/taobao/auth.js +0 -58
  1002. package/clis/taobao/cart.js +0 -96
  1003. package/clis/taobao/commands.test.js +0 -41
  1004. package/clis/taobao/detail.js +0 -71
  1005. package/clis/taobao/reviews.js +0 -77
  1006. package/clis/taobao/search.js +0 -97
  1007. package/clis/tdx/hot-rank.js +0 -48
  1008. package/clis/tdx/hot-rank.test.js +0 -59
  1009. package/clis/ths/hot-rank.js +0 -50
  1010. package/clis/ths/hot-rank.test.js +0 -64
  1011. package/clis/tieba/commands.test.js +0 -79
  1012. package/clis/tieba/hot.js +0 -48
  1013. package/clis/tieba/posts.js +0 -86
  1014. package/clis/tieba/read.js +0 -141
  1015. package/clis/tieba/search.js +0 -109
  1016. package/clis/tieba/utils.js +0 -240
  1017. package/clis/tieba/utils.test.js +0 -290
  1018. package/clis/toutiao/articles.js +0 -55
  1019. package/clis/toutiao/auth.js +0 -70
  1020. package/clis/toutiao/hot.js +0 -63
  1021. package/clis/toutiao/toutiao.test.js +0 -378
  1022. package/clis/toutiao/utils.js +0 -161
  1023. package/clis/trae-cn/activity.js +0 -22
  1024. package/clis/trae-cn/approve.js +0 -52
  1025. package/clis/trae-cn/ask.js +0 -99
  1026. package/clis/trae-cn/dump.js +0 -5
  1027. package/clis/trae-cn/export.js +0 -40
  1028. package/clis/trae-cn/model.js +0 -22
  1029. package/clis/trae-cn/new.js +0 -63
  1030. package/clis/trae-cn/read.js +0 -23
  1031. package/clis/trae-cn/screenshot.js +0 -5
  1032. package/clis/trae-cn/select-model.js +0 -25
  1033. package/clis/trae-cn/send.js +0 -23
  1034. package/clis/trae-cn/setup.js +0 -75
  1035. package/clis/trae-cn/status.js +0 -27
  1036. package/clis/trae-cn/targets.js +0 -96
  1037. package/clis/trae-cn/trae-cn.test.js +0 -412
  1038. package/clis/trae-cn/utils.js +0 -750
  1039. package/clis/trae-cn/watch.js +0 -119
  1040. package/clis/uisdc/news.js +0 -105
  1041. package/clis/uisdc/news.test.js +0 -66
  1042. package/clis/v2ex/auth.js +0 -44
  1043. package/clis/v2ex/daily.js +0 -99
  1044. package/clis/v2ex/hot.js +0 -26
  1045. package/clis/v2ex/latest.js +0 -26
  1046. package/clis/v2ex/me.js +0 -100
  1047. package/clis/v2ex/member.js +0 -28
  1048. package/clis/v2ex/node.js +0 -39
  1049. package/clis/v2ex/nodes.js +0 -26
  1050. package/clis/v2ex/notifications.js +0 -72
  1051. package/clis/v2ex/replies.js +0 -27
  1052. package/clis/v2ex/topic.js +0 -31
  1053. package/clis/v2ex/user.js +0 -34
  1054. package/clis/wanfang/search.js +0 -66
  1055. package/clis/wanfang/search.test.js +0 -23
  1056. package/clis/wechat-channels/auth.js +0 -62
  1057. package/clis/wechat-channels/publish.js +0 -732
  1058. package/clis/wechat-channels/publish.test.js +0 -144
  1059. package/clis/weibo/auth.js +0 -53
  1060. package/clis/weibo/comments.js +0 -53
  1061. package/clis/weibo/delete.js +0 -172
  1062. package/clis/weibo/delete.test.js +0 -94
  1063. package/clis/weibo/envelope.test.js +0 -85
  1064. package/clis/weibo/favorites.js +0 -170
  1065. package/clis/weibo/favorites.test.js +0 -114
  1066. package/clis/weibo/feed.js +0 -69
  1067. package/clis/weibo/hot.js +0 -39
  1068. package/clis/weibo/me.js +0 -75
  1069. package/clis/weibo/post.js +0 -75
  1070. package/clis/weibo/publish.js +0 -306
  1071. package/clis/weibo/publish.test.js +0 -192
  1072. package/clis/weibo/search.js +0 -80
  1073. package/clis/weibo/user-posts.js +0 -234
  1074. package/clis/weibo/user-posts.test.js +0 -92
  1075. package/clis/weibo/user.js +0 -63
  1076. package/clis/weibo/utils.js +0 -59
  1077. package/clis/weibo/utils.test.js +0 -36
  1078. package/clis/weixin/create-draft.js +0 -226
  1079. package/clis/weixin/download.js +0 -371
  1080. package/clis/weixin/download.test.js +0 -106
  1081. package/clis/weixin/drafts.js +0 -66
  1082. package/clis/weixin/drafts.test.js +0 -69
  1083. package/clis/weixin/search.js +0 -157
  1084. package/clis/weixin/search.test.js +0 -227
  1085. package/clis/weread/ai-outline.js +0 -171
  1086. package/clis/weread/ai-outline.test.js +0 -83
  1087. package/clis/weread/auth.js +0 -59
  1088. package/clis/weread/book-search.js +0 -438
  1089. package/clis/weread/book-search.test.js +0 -242
  1090. package/clis/weread/book.js +0 -233
  1091. package/clis/weread/commands.test.js +0 -398
  1092. package/clis/weread/highlights.js +0 -24
  1093. package/clis/weread/notebooks.js +0 -22
  1094. package/clis/weread/notes.js +0 -30
  1095. package/clis/weread/private-api-regression.test.js +0 -369
  1096. package/clis/weread/ranking.js +0 -29
  1097. package/clis/weread/search-regression.test.js +0 -494
  1098. package/clis/weread/search.js +0 -186
  1099. package/clis/weread/shelf.js +0 -66
  1100. package/clis/weread/utils.js +0 -321
  1101. package/clis/weread/utils.test.js +0 -128
  1102. package/clis/weread-official/book.js +0 -135
  1103. package/clis/weread-official/commands.test.js +0 -385
  1104. package/clis/weread-official/discover.js +0 -107
  1105. package/clis/weread-official/list-apis.js +0 -95
  1106. package/clis/weread-official/notes.js +0 -171
  1107. package/clis/weread-official/readdata.js +0 -158
  1108. package/clis/weread-official/review.js +0 -93
  1109. package/clis/weread-official/search.js +0 -106
  1110. package/clis/weread-official/shelf.js +0 -97
  1111. package/clis/weread-official/utils.js +0 -293
  1112. package/clis/weread-official/utils.test.js +0 -242
  1113. package/clis/xianyu/auth.js +0 -68
  1114. package/clis/xianyu/chat.js +0 -74
  1115. package/clis/xianyu/chat.test.js +0 -84
  1116. package/clis/xianyu/im.js +0 -322
  1117. package/clis/xianyu/im.test.js +0 -253
  1118. package/clis/xianyu/inbox.js +0 -96
  1119. package/clis/xianyu/item.js +0 -153
  1120. package/clis/xianyu/item.test.js +0 -56
  1121. package/clis/xianyu/messages.js +0 -91
  1122. package/clis/xianyu/publish.js +0 -485
  1123. package/clis/xianyu/publish.test.js +0 -220
  1124. package/clis/xianyu/reply.js +0 -82
  1125. package/clis/xianyu/search.js +0 -262
  1126. package/clis/xianyu/search.test.js +0 -104
  1127. package/clis/xianyu/utils.js +0 -8
  1128. package/clis/xiaoe/auth.js +0 -55
  1129. package/clis/xiaoe/catalog.js +0 -227
  1130. package/clis/xiaoe/content.js +0 -175
  1131. package/clis/xiaoe/courses.js +0 -127
  1132. package/clis/xiaoe/detail.js +0 -36
  1133. package/clis/xiaoe/play-url.js +0 -121
  1134. package/clis/xiaoe/xiaoe.test.js +0 -486
  1135. package/clis/xiaohongshu/ask.js +0 -426
  1136. package/clis/xiaohongshu/ask.test.js +0 -271
  1137. package/clis/xiaohongshu/auth.js +0 -53
  1138. package/clis/xiaohongshu/collection-helpers.js +0 -283
  1139. package/clis/xiaohongshu/collection-helpers.test.js +0 -125
  1140. package/clis/xiaohongshu/comments.js +0 -228
  1141. package/clis/xiaohongshu/comments.test.js +0 -320
  1142. package/clis/xiaohongshu/creator-note-detail.js +0 -482
  1143. package/clis/xiaohongshu/creator-note-detail.test.js +0 -451
  1144. package/clis/xiaohongshu/creator-notes-summary.js +0 -98
  1145. package/clis/xiaohongshu/creator-notes-summary.test.js +0 -94
  1146. package/clis/xiaohongshu/creator-notes.js +0 -479
  1147. package/clis/xiaohongshu/creator-notes.test.js +0 -281
  1148. package/clis/xiaohongshu/creator-profile.js +0 -55
  1149. package/clis/xiaohongshu/creator-stats.js +0 -76
  1150. package/clis/xiaohongshu/creator-stats.test.js +0 -24
  1151. package/clis/xiaohongshu/delete-note.js +0 -260
  1152. package/clis/xiaohongshu/delete-note.test.js +0 -172
  1153. package/clis/xiaohongshu/download.js +0 -249
  1154. package/clis/xiaohongshu/download.test.js +0 -339
  1155. package/clis/xiaohongshu/draft-clear.js +0 -91
  1156. package/clis/xiaohongshu/draft-delete.js +0 -103
  1157. package/clis/xiaohongshu/draft-open.js +0 -43
  1158. package/clis/xiaohongshu/draft-utils.js +0 -142
  1159. package/clis/xiaohongshu/drafts.js +0 -28
  1160. package/clis/xiaohongshu/drafts.test.js +0 -172
  1161. package/clis/xiaohongshu/feed.js +0 -163
  1162. package/clis/xiaohongshu/feed.test.js +0 -126
  1163. package/clis/xiaohongshu/follow.js +0 -226
  1164. package/clis/xiaohongshu/follow.test.js +0 -211
  1165. package/clis/xiaohongshu/liked.js +0 -29
  1166. package/clis/xiaohongshu/liked.test.js +0 -82
  1167. package/clis/xiaohongshu/navigation.test.js +0 -38
  1168. package/clis/xiaohongshu/note-helpers.js +0 -69
  1169. package/clis/xiaohongshu/note.js +0 -104
  1170. package/clis/xiaohongshu/note.test.js +0 -249
  1171. package/clis/xiaohongshu/notifications.js +0 -46
  1172. package/clis/xiaohongshu/publish.js +0 -1423
  1173. package/clis/xiaohongshu/publish.test.js +0 -1141
  1174. package/clis/xiaohongshu/saved.js +0 -29
  1175. package/clis/xiaohongshu/saved.test.js +0 -143
  1176. package/clis/xiaohongshu/search.js +0 -330
  1177. package/clis/xiaohongshu/search.test.js +0 -325
  1178. package/clis/xiaohongshu/unfollow.js +0 -285
  1179. package/clis/xiaohongshu/user-helpers.js +0 -78
  1180. package/clis/xiaohongshu/user-helpers.test.js +0 -160
  1181. package/clis/xiaohongshu/user.js +0 -140
  1182. package/clis/xiaohongshu/user.test.js +0 -141
  1183. package/clis/xiaoyuzhou/auth.js +0 -303
  1184. package/clis/xiaoyuzhou/auth.test.js +0 -124
  1185. package/clis/xiaoyuzhou/download.js +0 -54
  1186. package/clis/xiaoyuzhou/download.test.js +0 -135
  1187. package/clis/xiaoyuzhou/episode.js +0 -34
  1188. package/clis/xiaoyuzhou/podcast-episodes.js +0 -41
  1189. package/clis/xiaoyuzhou/podcast-episodes.test.js +0 -78
  1190. package/clis/xiaoyuzhou/podcast.js +0 -33
  1191. package/clis/xiaoyuzhou/transcript.js +0 -77
  1192. package/clis/xiaoyuzhou/transcript.test.js +0 -195
  1193. package/clis/xiaoyuzhou/utils.js +0 -15
  1194. package/clis/xiaoyuzhou/utils.test.js +0 -39
  1195. package/clis/xueqiu/auth.js +0 -63
  1196. package/clis/xueqiu/comments.js +0 -355
  1197. package/clis/xueqiu/comments.test.js +0 -696
  1198. package/clis/xueqiu/danjuan-utils.js +0 -126
  1199. package/clis/xueqiu/danjuan-utils.test.js +0 -41
  1200. package/clis/xueqiu/earnings-date.js +0 -45
  1201. package/clis/xueqiu/feed.js +0 -39
  1202. package/clis/xueqiu/fund-holdings.js +0 -29
  1203. package/clis/xueqiu/fund-snapshot.js +0 -26
  1204. package/clis/xueqiu/groups.js +0 -23
  1205. package/clis/xueqiu/hot-stock.js +0 -32
  1206. package/clis/xueqiu/hot.js +0 -37
  1207. package/clis/xueqiu/kline.js +0 -44
  1208. package/clis/xueqiu/search.js +0 -37
  1209. package/clis/xueqiu/stock.js +0 -65
  1210. package/clis/xueqiu/utils.js +0 -48
  1211. package/clis/xueqiu/utils.test.js +0 -26
  1212. package/clis/xueqiu/watchlist.js +0 -37
  1213. package/clis/youdao/note.js +0 -258
  1214. package/clis/youdao/note.test.js +0 -99
  1215. package/clis/yuanbao/ask.js +0 -362
  1216. package/clis/yuanbao/ask.test.js +0 -124
  1217. package/clis/yuanbao/auth.js +0 -59
  1218. package/clis/yuanbao/detail.js +0 -65
  1219. package/clis/yuanbao/history.js +0 -51
  1220. package/clis/yuanbao/new.js +0 -72
  1221. package/clis/yuanbao/new.test.js +0 -30
  1222. package/clis/yuanbao/read.js +0 -38
  1223. package/clis/yuanbao/send.js +0 -57
  1224. package/clis/yuanbao/shared.js +0 -341
  1225. package/clis/yuanbao/shared.test.js +0 -80
  1226. package/clis/yuanbao/status.js +0 -44
  1227. package/clis/zhihu/answer-comments.js +0 -280
  1228. package/clis/zhihu/answer-comments.test.js +0 -287
  1229. package/clis/zhihu/answer-detail.js +0 -216
  1230. package/clis/zhihu/answer-detail.test.js +0 -338
  1231. package/clis/zhihu/answer.js +0 -54
  1232. package/clis/zhihu/answer.test.js +0 -54
  1233. package/clis/zhihu/auth.js +0 -59
  1234. package/clis/zhihu/collection.js +0 -199
  1235. package/clis/zhihu/collection.test.js +0 -337
  1236. package/clis/zhihu/collections.js +0 -128
  1237. package/clis/zhihu/collections.test.js +0 -182
  1238. package/clis/zhihu/comment.js +0 -55
  1239. package/clis/zhihu/comment.test.js +0 -50
  1240. package/clis/zhihu/download.js +0 -80
  1241. package/clis/zhihu/download.test.js +0 -12
  1242. package/clis/zhihu/favorite.js +0 -87
  1243. package/clis/zhihu/favorite.test.js +0 -59
  1244. package/clis/zhihu/follow.js +0 -50
  1245. package/clis/zhihu/follow.test.js +0 -42
  1246. package/clis/zhihu/followers.js +0 -40
  1247. package/clis/zhihu/followers.test.js +0 -42
  1248. package/clis/zhihu/following.js +0 -40
  1249. package/clis/zhihu/following.test.js +0 -44
  1250. package/clis/zhihu/hot.js +0 -44
  1251. package/clis/zhihu/like.js +0 -47
  1252. package/clis/zhihu/like.test.js +0 -41
  1253. package/clis/zhihu/paginate.js +0 -128
  1254. package/clis/zhihu/pins.js +0 -41
  1255. package/clis/zhihu/pins.test.js +0 -44
  1256. package/clis/zhihu/question.js +0 -113
  1257. package/clis/zhihu/question.test.js +0 -251
  1258. package/clis/zhihu/recommend.js +0 -103
  1259. package/clis/zhihu/recommend.test.js +0 -143
  1260. package/clis/zhihu/search.js +0 -206
  1261. package/clis/zhihu/search.test.js +0 -198
  1262. package/clis/zhihu/target.js +0 -91
  1263. package/clis/zhihu/target.test.js +0 -77
  1264. package/clis/zhihu/text.js +0 -29
  1265. package/clis/zhihu/text.test.js +0 -24
  1266. package/clis/zhihu/user-answers.js +0 -41
  1267. package/clis/zhihu/user-answers.test.js +0 -69
  1268. package/clis/zhihu/user-arg.js +0 -34
  1269. package/clis/zhihu/user-articles.js +0 -40
  1270. package/clis/zhihu/user-articles.test.js +0 -44
  1271. package/clis/zhihu/user.js +0 -59
  1272. package/clis/zhihu/user.test.js +0 -73
  1273. package/clis/zhihu/write-shared.js +0 -228
  1274. package/clis/zhihu/write-shared.test.js +0 -176
  1275. package/clis/zsxq/auth.js +0 -68
  1276. package/clis/zsxq/dynamics.js +0 -48
  1277. package/clis/zsxq/groups.js +0 -33
  1278. package/clis/zsxq/search.js +0 -44
  1279. package/clis/zsxq/search.test.js +0 -24
  1280. package/clis/zsxq/topic.js +0 -50
  1281. package/clis/zsxq/topic.test.js +0 -30
  1282. package/clis/zsxq/topics.js +0 -26
  1283. package/clis/zsxq/topics.test.js +0 -24
  1284. package/clis/zsxq/utils.js +0 -233
  1285. package/clis/zsxq/utils.test.js +0 -31
  1286. package/skills/webcmd-adapter-author/references/site-memory/bilibili.md +0 -70
  1287. package/skills/webcmd-adapter-author/references/site-memory/eastmoney.md +0 -70
  1288. package/skills/webcmd-adapter-author/references/site-memory/tonghuashun.md +0 -51
  1289. package/skills/webcmd-adapter-author/references/site-memory/xueqiu.md +0 -58
  1290. /package/{clis/douyin/_shared/types.js → dist/src/adapter-shadow.test.d.ts} +0 -0
@@ -0,0 +1,3041 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import yaml from 'js-yaml';
6
+ import { cli, getRegistry, Strategy } from './registry.js';
7
+ import { BrowserCommandError } from './browser/daemon-client.js';
8
+ import { TargetError } from './browser/target-errors.js';
9
+ import { PKG_VERSION } from './version.js';
10
+ import { classifyAdapter } from './help.js';
11
+ const { mockBrowserConnect, mockBrowserClose, mockBindTab, mockSendCommand, mockExecFileSync, browserState, } = vi.hoisted(() => ({
12
+ mockBrowserConnect: vi.fn(),
13
+ mockBrowserClose: vi.fn(),
14
+ mockBindTab: vi.fn(),
15
+ mockSendCommand: vi.fn(),
16
+ mockExecFileSync: vi.fn(),
17
+ browserState: { page: null },
18
+ }));
19
+ vi.mock('./browser/index.js', () => {
20
+ mockBrowserConnect.mockImplementation(async () => browserState.page);
21
+ return {
22
+ BrowserBridge: class {
23
+ connect = mockBrowserConnect;
24
+ close = mockBrowserClose;
25
+ },
26
+ };
27
+ });
28
+ vi.mock('./browser/daemon-client.js', async () => {
29
+ const actual = await vi.importActual('./browser/daemon-client.js');
30
+ return {
31
+ ...actual,
32
+ bindTab: mockBindTab,
33
+ sendCommand: mockSendCommand,
34
+ };
35
+ });
36
+ vi.mock('node:child_process', async () => {
37
+ const actual = await vi.importActual('node:child_process');
38
+ return {
39
+ ...actual,
40
+ execFileSync: mockExecFileSync,
41
+ };
42
+ });
43
+ import { createProgram, findPackageRoot, normalizeVerifyRows, renderVerifyPreview, resolveBrowserVerifyInvocation, resolveSitemapAvailabilityForUrl, selectFreshByTimestamp } from './cli.js';
44
+ describe('createProgram root help descriptions', () => {
45
+ function descriptionFor(program, name) {
46
+ return program.commands.find(cmd => cmd.name() === name)?.description();
47
+ }
48
+ it('summarizes built-in command groups with their subcommands', () => {
49
+ const program = createProgram('', '');
50
+ expect(descriptionFor(program, 'browser')).toContain('open');
51
+ expect(descriptionFor(program, 'browser')).toContain('type');
52
+ expect(descriptionFor(program, 'browser')).toContain('verify');
53
+ expect(descriptionFor(program, 'browser')).not.toContain('Browser control');
54
+ expect(descriptionFor(program, 'auth')).toBe('refresh, status');
55
+ expect(descriptionFor(program, 'plugin')).toBe('create, install, list, uninstall, update');
56
+ expect(descriptionFor(program, 'adapter')).toBe('eject, reset, status');
57
+ expect(descriptionFor(program, 'profile')).toBe('list, rename, use');
58
+ expect(descriptionFor(program, 'daemon')).toBe('restart, status, stop');
59
+ expect(descriptionFor(program, 'external')).toBe('install, list, register');
60
+ });
61
+ it('renders auth namespace structured help', () => {
62
+ const argv = process.argv;
63
+ try {
64
+ const program = createProgram('', '');
65
+ const auth = program.commands.find(cmd => cmd.name() === 'auth');
66
+ expect(auth).toBeTruthy();
67
+ process.argv = ['node', 'webcmd', 'auth', '--help', '-f', 'yaml'];
68
+ const data = yaml.load(auth.helpInformation());
69
+ expect(data).toMatchObject({
70
+ namespace: 'auth',
71
+ description: 'Inspect website login status',
72
+ command_count: 2,
73
+ });
74
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['refresh', 'status']);
75
+ const status = auth.commands.find(cmd => cmd.name() === 'status');
76
+ process.argv = ['node', 'webcmd', 'auth', 'status', '--help', '-f', 'yaml'];
77
+ const statusData = yaml.load(status.helpInformation());
78
+ expect(statusData.command).toBe('webcmd auth status');
79
+ expect(statusData.command_options.map((option) => option.name)).toEqual(expect.arrayContaining([
80
+ 'site',
81
+ 'full',
82
+ 'concurrency',
83
+ 'timeout',
84
+ 'only',
85
+ 'format',
86
+ ]));
87
+ }
88
+ finally {
89
+ process.argv = argv;
90
+ }
91
+ });
92
+ it('keeps leaf command descriptions unchanged', () => {
93
+ const program = createProgram('', '');
94
+ expect(descriptionFor(program, 'list')).toBe('List all available CLI commands');
95
+ expect(descriptionFor(program, 'doctor')).toBe('Diagnose webcmd browser bridge connectivity');
96
+ });
97
+ it('keeps site adapters out of root commands and lists sites in the root help tail', () => {
98
+ const registry = getRegistry();
99
+ const snapshot = new Map(registry);
100
+ registry.clear();
101
+ try {
102
+ cli({
103
+ site: 'reddit',
104
+ name: 'hot',
105
+ access: 'read',
106
+ description: 'Reddit hot posts',
107
+ strategy: Strategy.PUBLIC,
108
+ browser: false,
109
+ });
110
+ cli({
111
+ site: 'youtube',
112
+ name: 'search',
113
+ access: 'read',
114
+ description: 'Search YouTube',
115
+ strategy: Strategy.PUBLIC,
116
+ browser: false,
117
+ });
118
+ const program = createProgram('', '');
119
+ const help = program.helpInformation();
120
+ expect(help).toContain('Site adapters (2):');
121
+ expect(help).toContain('reddit, youtube');
122
+ expect(help).toContain("webcmd <site> --help -f yaml");
123
+ expect(help).not.toMatch(/\n reddit\s+hot/);
124
+ expect(help).not.toMatch(/\n youtube\s+search/);
125
+ }
126
+ finally {
127
+ registry.clear();
128
+ for (const [key, value] of snapshot)
129
+ registry.set(key, value);
130
+ }
131
+ });
132
+ it('groups adapters into App / Site buckets by domain field', () => {
133
+ const registry = getRegistry();
134
+ const snapshot = new Map(registry);
135
+ registry.clear();
136
+ try {
137
+ cli({
138
+ site: 'youtube',
139
+ name: 'search',
140
+ access: 'read',
141
+ description: 'Search YouTube',
142
+ domain: 'www.youtube.com',
143
+ strategy: Strategy.PUBLIC,
144
+ browser: false,
145
+ });
146
+ cli({
147
+ site: 'chatwise',
148
+ name: 'ask',
149
+ access: 'write',
150
+ description: 'Ask Chatwise desktop app',
151
+ domain: 'localhost',
152
+ strategy: Strategy.UI,
153
+ browser: true,
154
+ });
155
+ const program = createProgram('', '');
156
+ const help = program.helpInformation();
157
+ // Two separate sections, each with own count
158
+ expect(help).toContain('App adapters (1):');
159
+ expect(help).toMatch(/App adapters \(1\):\n {2}chatwise/);
160
+ expect(help).toContain('Site adapters (1):');
161
+ expect(help).toMatch(/Site adapters \(1\):\n {2}youtube/);
162
+ // App adapters appear before Site adapters (External CLIs are absent here)
163
+ expect(help.indexOf('App adapters')).toBeLessThan(help.indexOf('Site adapters'));
164
+ }
165
+ finally {
166
+ registry.clear();
167
+ for (const [key, value] of snapshot)
168
+ registry.set(key, value);
169
+ }
170
+ });
171
+ it('classifies local IP domains as app adapters', () => {
172
+ expect(classifyAdapter('localhost')).toBe('app');
173
+ expect(classifyAdapter('127.0.0.1')).toBe('app');
174
+ expect(classifyAdapter('::1')).toBe('app');
175
+ expect(classifyAdapter('www.youtube.com')).toBe('site');
176
+ });
177
+ it('splits list table output into App and Site sections without changing per-site rows', async () => {
178
+ const registry = getRegistry();
179
+ const snapshot = new Map(registry);
180
+ const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
181
+ const restoreStdoutSpy = () => stdoutSpy.mockImplementation(() => { });
182
+ registry.clear();
183
+ try {
184
+ cli({
185
+ site: 'antigravity',
186
+ name: 'history',
187
+ access: 'read',
188
+ description: 'Read Antigravity history',
189
+ domain: '127.0.0.1',
190
+ strategy: Strategy.UI,
191
+ browser: true,
192
+ });
193
+ cli({
194
+ site: 'chatwise',
195
+ name: 'ask',
196
+ access: 'write',
197
+ description: 'Ask Chatwise desktop app',
198
+ domain: 'localhost',
199
+ strategy: Strategy.UI,
200
+ browser: true,
201
+ });
202
+ cli({
203
+ site: 'youtube',
204
+ name: 'search',
205
+ access: 'read',
206
+ description: 'Search YouTube',
207
+ domain: 'www.youtube.com',
208
+ strategy: Strategy.PUBLIC,
209
+ browser: false,
210
+ });
211
+ const program = createProgram('', '');
212
+ await program.parseAsync(['node', 'webcmd', 'list']);
213
+ const output = stdoutSpy.mock.calls.flat().join('\n');
214
+ expect(output).toContain('App adapters');
215
+ expect(output).toContain('Site adapters');
216
+ expect(output.indexOf('App adapters')).toBeLessThan(output.indexOf('Site adapters'));
217
+ expect(output).toMatch(/App adapters[\s\S]*antigravity[\s\S]*history \[ui\] — Read Antigravity history/);
218
+ expect(output).toMatch(/App adapters[\s\S]*chatwise[\s\S]*ask \[ui\] — Ask Chatwise desktop app/);
219
+ expect(output).toMatch(/Site adapters[\s\S]*youtube[\s\S]*search \[public\] — Search YouTube/);
220
+ expect(output).toContain('3 built-in commands across 2 apps + 1 sites,');
221
+ }
222
+ finally {
223
+ restoreStdoutSpy();
224
+ stdoutSpy.mockClear();
225
+ registry.clear();
226
+ for (const [key, value] of snapshot)
227
+ registry.set(key, value);
228
+ }
229
+ });
230
+ it('omits empty list table sections and leaves structured list rows unchanged', async () => {
231
+ const registry = getRegistry();
232
+ const snapshot = new Map(registry);
233
+ const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
234
+ const restoreStdoutSpy = () => stdoutSpy.mockImplementation(() => { });
235
+ registry.clear();
236
+ try {
237
+ cli({
238
+ site: 'youtube',
239
+ name: 'search',
240
+ access: 'read',
241
+ description: 'Search YouTube',
242
+ domain: 'www.youtube.com',
243
+ strategy: Strategy.PUBLIC,
244
+ browser: false,
245
+ columns: ['title', 'url'],
246
+ });
247
+ const tableProgram = createProgram('', '');
248
+ await tableProgram.parseAsync(['node', 'webcmd', 'list']);
249
+ const tableOutput = stdoutSpy.mock.calls.flat().join('\n');
250
+ expect(tableOutput).not.toContain('App adapters');
251
+ expect(tableOutput).toContain('Site adapters');
252
+ expect(tableOutput).toContain('1 built-in commands across 0 apps + 1 sites,');
253
+ stdoutSpy.mockClear();
254
+ const jsonProgram = createProgram('', '');
255
+ await jsonProgram.parseAsync(['node', 'webcmd', 'list', '-f', 'json']);
256
+ const jsonOutput = stdoutSpy.mock.calls.flat().join('\n');
257
+ const rows = JSON.parse(jsonOutput);
258
+ expect(rows).toMatchObject([
259
+ {
260
+ site: 'youtube',
261
+ name: 'search',
262
+ domain: 'www.youtube.com',
263
+ columns: ['title', 'url'],
264
+ },
265
+ ]);
266
+ expect(rows[0]).not.toHaveProperty('adapterKind');
267
+ expect(rows[0]).not.toHaveProperty('section');
268
+ }
269
+ finally {
270
+ restoreStdoutSpy();
271
+ stdoutSpy.mockClear();
272
+ registry.clear();
273
+ for (const [key, value] of snapshot)
274
+ registry.set(key, value);
275
+ }
276
+ });
277
+ it('exposes external_clis / app_adapters / site_adapters in structured help', () => {
278
+ const registry = getRegistry();
279
+ const snapshot = new Map(registry);
280
+ const argv = process.argv;
281
+ registry.clear();
282
+ try {
283
+ cli({
284
+ site: 'youtube',
285
+ name: 'search',
286
+ access: 'read',
287
+ description: 'Search YouTube',
288
+ domain: 'www.youtube.com',
289
+ strategy: Strategy.PUBLIC,
290
+ browser: false,
291
+ });
292
+ cli({
293
+ site: 'chatwise',
294
+ name: 'ask',
295
+ access: 'write',
296
+ description: 'Ask Chatwise desktop app',
297
+ domain: 'localhost',
298
+ strategy: Strategy.UI,
299
+ browser: true,
300
+ });
301
+ const program = createProgram('', '');
302
+ process.argv = ['node', 'webcmd', '--help', '-f', 'yaml'];
303
+ const data = yaml.load(program.helpInformation());
304
+ expect(data.app_adapters.count).toBe(1);
305
+ expect(data.app_adapters.apps).toEqual(['chatwise']);
306
+ expect(data.site_adapters.count).toBe(1);
307
+ expect(data.site_adapters.sites).toEqual(['youtube']);
308
+ expect(data.external_clis.count).toBeGreaterThanOrEqual(0);
309
+ expect(Array.isArray(data.external_clis.clis)).toBe(true);
310
+ expect(Array.isArray(data.external_clis.display)).toBe(true);
311
+ // Adapters must NOT leak into the core commands list
312
+ const commandNames = data.commands.map((cmd) => cmd.name);
313
+ expect(commandNames).not.toContain('youtube');
314
+ expect(commandNames).not.toContain('chatwise');
315
+ }
316
+ finally {
317
+ process.argv = argv;
318
+ registry.clear();
319
+ for (const [key, value] of snapshot)
320
+ registry.set(key, value);
321
+ }
322
+ });
323
+ it('renders root structured help with built-ins and site adapter names', () => {
324
+ const registry = getRegistry();
325
+ const snapshot = new Map(registry);
326
+ const argv = process.argv;
327
+ registry.clear();
328
+ try {
329
+ cli({
330
+ site: 'youtube',
331
+ name: 'search',
332
+ access: 'read',
333
+ description: 'Search YouTube',
334
+ strategy: Strategy.PUBLIC,
335
+ browser: false,
336
+ });
337
+ const program = createProgram('', '');
338
+ process.argv = ['node', 'webcmd', '--help', '-f', 'yaml'];
339
+ const data = yaml.load(program.helpInformation());
340
+ expect(data.site_adapters.count).toBe(1);
341
+ expect(data.site_adapters.sites).toEqual(['youtube']);
342
+ expect(data.commands.map((cmd) => cmd.name)).toContain('list');
343
+ expect(data.commands.map((cmd) => cmd.name)).not.toContain('youtube');
344
+ }
345
+ finally {
346
+ process.argv = argv;
347
+ registry.clear();
348
+ for (const [key, value] of snapshot)
349
+ registry.set(key, value);
350
+ }
351
+ });
352
+ it('renders per-site structured help with all commands, access, args, and examples', () => {
353
+ const registry = getRegistry();
354
+ const snapshot = new Map(registry);
355
+ const argv = process.argv;
356
+ registry.clear();
357
+ try {
358
+ cli({
359
+ site: 'youtube',
360
+ name: 'search',
361
+ access: 'read',
362
+ description: 'Search YouTube',
363
+ strategy: Strategy.PUBLIC,
364
+ browser: false,
365
+ args: [{ name: 'limit', type: 'int', default: 20, help: 'Number of videos' }],
366
+ columns: ['title', 'url'],
367
+ });
368
+ const program = createProgram('', '');
369
+ const site = program.commands.find(cmd => cmd.name() === 'youtube');
370
+ expect(site).toBeTruthy();
371
+ process.argv = ['node', 'webcmd', 'youtube', '--help', '-f', 'yaml'];
372
+ const data = yaml.load(site.helpInformation());
373
+ expect(data.site).toBe('youtube');
374
+ expect(data.commands).toMatchObject([
375
+ {
376
+ name: 'search',
377
+ access: 'read',
378
+ description: 'Search YouTube',
379
+ browser: false,
380
+ example: 'webcmd youtube search -f yaml',
381
+ command_options: [{ name: 'limit', type: 'int', default: 20 }],
382
+ columns: ['title', 'url'],
383
+ },
384
+ ]);
385
+ expect(data.commands[0]).not.toHaveProperty('args');
386
+ }
387
+ finally {
388
+ process.argv = argv;
389
+ registry.clear();
390
+ for (const [key, value] of snapshot)
391
+ registry.set(key, value);
392
+ }
393
+ });
394
+ it('renders per-site text help without per-command common option noise', () => {
395
+ const registry = getRegistry();
396
+ const snapshot = new Map(registry);
397
+ registry.clear();
398
+ try {
399
+ cli({
400
+ site: 'youtube',
401
+ name: 'search',
402
+ access: 'read',
403
+ description: 'Search YouTube',
404
+ strategy: Strategy.PUBLIC,
405
+ browser: false,
406
+ args: [{ name: 'limit', type: 'int', default: 20, help: 'Number of videos' }],
407
+ });
408
+ cli({
409
+ site: 'youtube',
410
+ name: 'video',
411
+ access: 'read',
412
+ description: 'Read one video',
413
+ domain: 'www.youtube.com',
414
+ strategy: Strategy.PUBLIC,
415
+ browser: true,
416
+ args: [{ name: 'bvid', positional: true, required: true, help: 'Video id' }],
417
+ });
418
+ const program = createProgram('', '');
419
+ const site = program.commands.find(cmd => cmd.name() === 'youtube');
420
+ expect(site).toBeTruthy();
421
+ const help = site.helpInformation();
422
+ expect(help).toContain('search [options] [read] Search YouTube');
423
+ expect(help).toContain('video <bvid> [read] Read one video');
424
+ expect(help).toContain('search [options]');
425
+ expect(help).not.toContain('video <bvid> [options]');
426
+ expect(help).not.toContain('\nOptions:');
427
+ expect(help).toContain('Common options:');
428
+ expect(help).toContain('-f, --format <fmt>');
429
+ expect(help).toContain('--trace <mode>');
430
+ expect(help).toContain('get all command args/options in one structured response');
431
+ }
432
+ finally {
433
+ registry.clear();
434
+ for (const [key, value] of snapshot)
435
+ registry.set(key, value);
436
+ }
437
+ });
438
+ it('separates command args from common options in structured help', () => {
439
+ const registry = getRegistry();
440
+ const snapshot = new Map(registry);
441
+ const argv = process.argv;
442
+ registry.clear();
443
+ try {
444
+ cli({
445
+ site: 'youtube',
446
+ name: 'video',
447
+ access: 'read',
448
+ description: 'Read one video',
449
+ strategy: Strategy.PUBLIC,
450
+ domain: 'www.youtube.com',
451
+ browser: true,
452
+ args: [
453
+ { name: 'bvid', positional: true, required: true, help: 'Video id' },
454
+ { name: 'with-comments', type: 'boolean', default: false, help: 'Include comments' },
455
+ ],
456
+ columns: ['title', 'url'],
457
+ });
458
+ const program = createProgram('', '');
459
+ const site = program.commands.find(cmd => cmd.name() === 'youtube');
460
+ const command = site.commands.find(cmd => cmd.name() === 'video');
461
+ expect(command).toBeTruthy();
462
+ process.argv = ['node', 'webcmd', 'youtube', 'video', '--help', '-f', 'yaml'];
463
+ const data = yaml.load(command.helpInformation());
464
+ expect(data.usage).toBe('webcmd youtube video <bvid> [options]');
465
+ expect(data.browser).toBe(true);
466
+ expect(data.domain).toBe('www.youtube.com');
467
+ expect(data.positionals).toMatchObject([{ name: 'bvid', positional: true, required: true }]);
468
+ expect(data.command_options).toMatchObject([{ name: 'with-comments', default: false }]);
469
+ expect(data.common_options.map((option) => option.name)).toEqual(['format', 'trace', 'verbose', 'help']);
470
+ expect(data.columns).toEqual(['title', 'url']);
471
+ expect(data).not.toHaveProperty('args');
472
+ }
473
+ finally {
474
+ process.argv = argv;
475
+ registry.clear();
476
+ for (const [key, value] of snapshot)
477
+ registry.set(key, value);
478
+ }
479
+ });
480
+ it('renders browser namespace structured help from Commander metadata', () => {
481
+ const argv = process.argv;
482
+ try {
483
+ const program = createProgram('', '');
484
+ const browser = program.commands.find(cmd => cmd.name() === 'browser');
485
+ expect(browser).toBeTruthy();
486
+ process.argv = ['node', 'webcmd', 'browser', '--session', 'test', '--help', '-f', 'yaml'];
487
+ const data = yaml.load(browser.helpInformation());
488
+ expect(data.namespace).toBe('browser');
489
+ expect(data.command).toBe('webcmd browser');
490
+ expect(data.description).toBe('Browser control — navigate, click, type, extract, wait (no LLM needed)');
491
+ expect(data.command_count).toBeGreaterThan(20);
492
+ // `--session` is now a hidden internal option; user-facing surface is the
493
+ // <session> positional declared via `.usage()`. Structured help drops
494
+ // hidden options, so namespace_options shouldn't expose it.
495
+ expect(data.namespace_options).not.toEqual(expect.arrayContaining([
496
+ expect.objectContaining({ name: 'session' }),
497
+ ]));
498
+ expect(data.namespace_options).toEqual(expect.arrayContaining([
499
+ expect.objectContaining({
500
+ name: 'window',
501
+ flags: '--window <mode>',
502
+ takes_value: 'required',
503
+ }),
504
+ ]));
505
+ expect(data.usage).toBe('webcmd browser <session> <command> [options]');
506
+ expect(data.global_options).toEqual(expect.arrayContaining([
507
+ expect.objectContaining({
508
+ name: 'version',
509
+ flags: '-V, --version',
510
+ }),
511
+ expect.objectContaining({
512
+ name: 'profile',
513
+ flags: '--profile <name>',
514
+ takes_value: 'required',
515
+ }),
516
+ ]));
517
+ const click = data.commands.find((cmd) => cmd.name === 'click');
518
+ // Structured help command/usage paths include the <session> positional so
519
+ // agents construct the correct full invocation. `name` is the leaf
520
+ // identifier (placeholder positionals are stripped).
521
+ expect(click).toMatchObject({
522
+ command: 'webcmd browser <session> click',
523
+ usage: 'webcmd browser <session> click [target] [options]',
524
+ positionals: [{ name: 'target' }],
525
+ });
526
+ expect(click.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
527
+ const tabList = data.commands.find((cmd) => cmd.name === 'tab list');
528
+ expect(tabList).toMatchObject({
529
+ command: 'webcmd browser <session> tab list',
530
+ usage: 'webcmd browser <session> tab list [options]',
531
+ command_options: [],
532
+ });
533
+ const getText = data.commands.find((cmd) => cmd.name === 'get text');
534
+ expect(getText).toMatchObject({
535
+ command: 'webcmd browser <session> get text',
536
+ positionals: [{ name: 'target' }],
537
+ });
538
+ expect(data.structured_help).toMatchObject({
539
+ formats: ['yaml', 'json'],
540
+ usage: 'webcmd browser --help -f yaml',
541
+ });
542
+ }
543
+ finally {
544
+ process.argv = argv;
545
+ }
546
+ });
547
+ it('renders nested browser parent structured help for a subtree', () => {
548
+ const argv = process.argv;
549
+ try {
550
+ const program = createProgram('', '');
551
+ const browser = program.commands.find(cmd => cmd.name() === 'browser');
552
+ const tab = browser.commands.find(cmd => cmd.name() === 'tab');
553
+ expect(tab).toBeTruthy();
554
+ process.argv = ['node', 'webcmd', 'browser', '--session', 'test', 'tab', '--help', '-f', 'yaml'];
555
+ const data = yaml.load(tab.helpInformation());
556
+ expect(data).toMatchObject({
557
+ namespace: 'browser',
558
+ group: 'tab',
559
+ command: 'webcmd browser <session> tab',
560
+ usage: 'webcmd browser <session> tab <command> [args] [options]',
561
+ command_count: 4,
562
+ });
563
+ expect(data.commands.map((cmd) => cmd.name)).toEqual([
564
+ 'tab close',
565
+ 'tab list',
566
+ 'tab new',
567
+ 'tab select',
568
+ ]);
569
+ expect(data.commands.find((cmd) => cmd.name === 'tab close')).toMatchObject({
570
+ command: 'webcmd browser <session> tab close',
571
+ usage: 'webcmd browser <session> tab close [targetId] [options]',
572
+ positionals: [{ name: 'targetId', help: 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"' }],
573
+ });
574
+ // session is now a hidden internal option (consumed from the <session> positional).
575
+ // namespace_options should only list user-facing options.
576
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['window']);
577
+ expect(data.structured_help).toMatchObject({
578
+ usage: 'webcmd browser <session> tab --help -f yaml',
579
+ });
580
+ }
581
+ finally {
582
+ process.argv = argv;
583
+ }
584
+ });
585
+ it('renders browser command structured help without needing the full namespace dump', () => {
586
+ const argv = process.argv;
587
+ try {
588
+ const program = createProgram('', '');
589
+ const browser = program.commands.find(cmd => cmd.name() === 'browser');
590
+ const click = browser.commands.find(cmd => cmd.name() === 'click');
591
+ expect(click).toBeTruthy();
592
+ process.argv = ['node', 'webcmd', 'browser', '--session', 'test', 'click', '--help', '-f', 'yaml'];
593
+ const data = yaml.load(click.helpInformation());
594
+ expect(data).toMatchObject({
595
+ namespace: 'browser',
596
+ name: 'click',
597
+ command: 'webcmd browser <session> click',
598
+ usage: 'webcmd browser <session> click [target] [options]',
599
+ positionals: [{ name: 'target' }],
600
+ structured_help: {
601
+ usage: 'webcmd browser <session> click --help -f yaml',
602
+ },
603
+ });
604
+ expect(data.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
605
+ // session is hidden; only `window` surfaces as a namespace option.
606
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['window']);
607
+ expect(data.global_options.map((option) => option.name)).toContain('profile');
608
+ }
609
+ finally {
610
+ process.argv = argv;
611
+ }
612
+ });
613
+ it('renders daemon namespace structured help with leaves and global options', () => {
614
+ const argv = process.argv;
615
+ try {
616
+ const program = createProgram('', '');
617
+ const daemon = program.commands.find(cmd => cmd.name() === 'daemon');
618
+ expect(daemon).toBeTruthy();
619
+ process.argv = ['node', 'webcmd', 'daemon', '--help', '-f', 'yaml'];
620
+ const data = yaml.load(daemon.helpInformation());
621
+ expect(data).toMatchObject({
622
+ namespace: 'daemon',
623
+ command: 'webcmd daemon',
624
+ usage: 'webcmd daemon <command> [args] [options]',
625
+ description: 'Manage the webcmd daemon',
626
+ command_count: 3,
627
+ namespace_options: [],
628
+ structured_help: { usage: 'webcmd daemon --help -f yaml' },
629
+ });
630
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['restart', 'status', 'stop']);
631
+ expect(data.global_options.map((option) => option.name)).toEqual(expect.arrayContaining(['version', 'profile']));
632
+ }
633
+ finally {
634
+ process.argv = argv;
635
+ }
636
+ });
637
+ it('renders plugin namespace structured help with positional + option leaves', () => {
638
+ const argv = process.argv;
639
+ try {
640
+ const program = createProgram('', '');
641
+ const plugin = program.commands.find(cmd => cmd.name() === 'plugin');
642
+ expect(plugin).toBeTruthy();
643
+ process.argv = ['node', 'webcmd', 'plugin', '--help', '-f', 'yaml'];
644
+ const data = yaml.load(plugin.helpInformation());
645
+ expect(data).toMatchObject({
646
+ namespace: 'plugin',
647
+ command: 'webcmd plugin',
648
+ description: 'Manage webcmd plugins',
649
+ namespace_options: [],
650
+ });
651
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['create', 'install', 'list', 'uninstall', 'update']);
652
+ const update = data.commands.find((cmd) => cmd.name === 'update');
653
+ expect(update).toMatchObject({
654
+ usage: 'webcmd plugin update [name] [options]',
655
+ positionals: [{ name: 'name' }],
656
+ });
657
+ expect(update.command_options.map((option) => option.name)).toEqual(['all']);
658
+ }
659
+ finally {
660
+ process.argv = argv;
661
+ }
662
+ });
663
+ it('renders adapter namespace structured help preserving original description after applyRootSubcommandSummaries', () => {
664
+ const argv = process.argv;
665
+ try {
666
+ const program = createProgram('', '');
667
+ const adapter = program.commands.find(cmd => cmd.name() === 'adapter');
668
+ expect(adapter).toBeTruthy();
669
+ process.argv = ['node', 'webcmd', 'adapter', '--help', '-f', 'yaml'];
670
+ const data = yaml.load(adapter.helpInformation());
671
+ // applyRootSubcommandSummaries() rewrites .description() to a child-name listing;
672
+ // structured help must surface the original product description via the snapshot.
673
+ expect(data.description).toBe('Manage CLI adapters');
674
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['eject', 'reset', 'status']);
675
+ const reset = data.commands.find((cmd) => cmd.name === 'reset');
676
+ expect(reset).toMatchObject({
677
+ usage: 'webcmd adapter reset [site] [options]',
678
+ positionals: [{ name: 'site' }],
679
+ });
680
+ expect(reset.command_options.map((option) => option.name)).toEqual(['all']);
681
+ }
682
+ finally {
683
+ process.argv = argv;
684
+ }
685
+ });
686
+ it('renders profile namespace structured help including required positionals', () => {
687
+ const argv = process.argv;
688
+ try {
689
+ const program = createProgram('', '');
690
+ const profile = program.commands.find(cmd => cmd.name() === 'profile');
691
+ expect(profile).toBeTruthy();
692
+ process.argv = ['node', 'webcmd', 'profile', '--help', '-f', 'yaml'];
693
+ const data = yaml.load(profile.helpInformation());
694
+ expect(data).toMatchObject({
695
+ namespace: 'profile',
696
+ description: 'Manage webcmd browser runtime profiles',
697
+ command_count: 3,
698
+ });
699
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['list', 'rename', 'use']);
700
+ const list = data.commands.find((cmd) => cmd.name === 'list');
701
+ expect(list).toMatchObject({
702
+ description: 'List Chrome and Chromium profiles available through the Cloak runtime',
703
+ });
704
+ const rename = data.commands.find((cmd) => cmd.name === 'rename');
705
+ expect(rename).toMatchObject({
706
+ usage: 'webcmd profile rename <contextId> <alias> [options]',
707
+ description: 'Assign a local alias to an available Cloak profile',
708
+ positionals: [
709
+ { name: 'contextId', required: true },
710
+ { name: 'alias', required: true },
711
+ ],
712
+ });
713
+ const use = data.commands.find((cmd) => cmd.name === 'use');
714
+ expect(use).toMatchObject({
715
+ description: 'Set the default Cloak profile for future commands',
716
+ });
717
+ }
718
+ finally {
719
+ process.argv = argv;
720
+ }
721
+ });
722
+ });
723
+ describe('resolveBrowserVerifyInvocation', () => {
724
+ it('prefers the built entry declared in package metadata', () => {
725
+ const projectRoot = path.join('repo-root');
726
+ const exists = new Set([
727
+ path.join(projectRoot, 'dist', 'src', 'main.js'),
728
+ ]);
729
+ expect(resolveBrowserVerifyInvocation({
730
+ projectRoot,
731
+ readFile: () => JSON.stringify({ bin: { webcmd: 'dist/src/main.js' } }),
732
+ fileExists: (candidate) => exists.has(candidate),
733
+ })).toEqual({
734
+ binary: process.execPath,
735
+ args: [path.join(projectRoot, 'dist', 'src', 'main.js')],
736
+ cwd: projectRoot,
737
+ });
738
+ });
739
+ it('falls back to compatibility built-entry candidates when package metadata is unavailable', () => {
740
+ const projectRoot = path.join('repo-root');
741
+ const exists = new Set([
742
+ path.join(projectRoot, 'dist', 'src', 'main.js'),
743
+ ]);
744
+ expect(resolveBrowserVerifyInvocation({
745
+ projectRoot,
746
+ readFile: () => { throw new Error('no package json'); },
747
+ fileExists: (candidate) => exists.has(candidate),
748
+ })).toEqual({
749
+ binary: process.execPath,
750
+ args: [path.join(projectRoot, 'dist', 'src', 'main.js')],
751
+ cwd: projectRoot,
752
+ });
753
+ });
754
+ it('falls back to the local tsx binary in source checkouts on Windows', () => {
755
+ const projectRoot = path.join('repo-root');
756
+ const exists = new Set([
757
+ path.join(projectRoot, 'src', 'main.ts'),
758
+ path.join(projectRoot, 'node_modules', '.bin', 'tsx.cmd'),
759
+ ]);
760
+ expect(resolveBrowserVerifyInvocation({
761
+ projectRoot,
762
+ platform: 'win32',
763
+ fileExists: (candidate) => exists.has(candidate),
764
+ })).toEqual({
765
+ binary: path.join(projectRoot, 'node_modules', '.bin', 'tsx.cmd'),
766
+ args: [path.join(projectRoot, 'src', 'main.ts')],
767
+ cwd: projectRoot,
768
+ shell: true,
769
+ });
770
+ });
771
+ it('falls back to npx tsx when local tsx is unavailable', () => {
772
+ const projectRoot = path.join('repo-root');
773
+ const exists = new Set([
774
+ path.join(projectRoot, 'src', 'main.ts'),
775
+ ]);
776
+ expect(resolveBrowserVerifyInvocation({
777
+ projectRoot,
778
+ platform: 'linux',
779
+ fileExists: (candidate) => exists.has(candidate),
780
+ })).toEqual({
781
+ binary: 'npx',
782
+ args: ['tsx', path.join(projectRoot, 'src', 'main.ts')],
783
+ cwd: projectRoot,
784
+ });
785
+ });
786
+ });
787
+ describe('selectFreshByTimestamp', () => {
788
+ it('uses timestamp watermarks so rolled buffers still emit new messages', () => {
789
+ const first = selectFreshByTimestamp([
790
+ { timestamp: 1, text: 'a' },
791
+ { timestamp: 2, text: 'b' },
792
+ ], 0);
793
+ expect(first.fresh.map((item) => item.text)).toEqual(['a', 'b']);
794
+ expect(first.lastSeenTs).toBe(2);
795
+ const rolled = selectFreshByTimestamp([
796
+ { timestamp: 2, text: 'b' },
797
+ { timestamp: 3, text: 'c' },
798
+ ], first.lastSeenTs);
799
+ expect(rolled.fresh.map((item) => item.text)).toEqual(['c']);
800
+ expect(rolled.lastSeenTs).toBe(3);
801
+ });
802
+ });
803
+ describe('resolveSitemapAvailabilityForUrl', () => {
804
+ function registryFor(site, domain) {
805
+ return new Map([[`${site}:read`, {
806
+ site,
807
+ name: 'read',
808
+ access: 'read',
809
+ description: 'read',
810
+ domain,
811
+ browser: false,
812
+ args: [],
813
+ }]]);
814
+ }
815
+ it('detects local sitemap overlays using adapter registry domain matches', () => {
816
+ const homeDir = path.join(os.tmpdir(), 'webcmd-sitemap-home');
817
+ const packageRoot = path.join(os.tmpdir(), 'webcmd-sitemap-package');
818
+ const localSitemap = path.join(homeDir, '.webcmd', 'sites', 'hackernews', 'sitemap');
819
+ const exists = new Set([localSitemap]);
820
+ const report = resolveSitemapAvailabilityForUrl('https://news.ycombinator.com/item?id=1', {
821
+ homeDir,
822
+ packageRoot,
823
+ registry: registryFor('hackernews', 'news.ycombinator.com'),
824
+ fileExists: (candidate) => exists.has(candidate),
825
+ });
826
+ expect(report).toMatchObject({
827
+ site: 'hackernews',
828
+ available: true,
829
+ source: 'local',
830
+ paths: { local: localSitemap },
831
+ });
832
+ expect(report?.hint).toContain('webcmd-browser-sitemap');
833
+ });
834
+ it('reports global+local when both sitemap layers exist', () => {
835
+ const homeDir = path.join(os.tmpdir(), 'webcmd-sitemap-home');
836
+ const packageRoot = path.join(os.tmpdir(), 'webcmd-sitemap-package');
837
+ const localSitemap = path.join(homeDir, '.webcmd', 'sites', 'twitter', 'sitemap.md');
838
+ const globalSitemap = path.join(packageRoot, 'sitemaps', 'twitter');
839
+ const exists = new Set([localSitemap, globalSitemap]);
840
+ const report = resolveSitemapAvailabilityForUrl('https://x.com/webcmd', {
841
+ homeDir,
842
+ packageRoot,
843
+ registry: registryFor('twitter', 'x.com'),
844
+ fileExists: (candidate) => exists.has(candidate),
845
+ });
846
+ expect(report).toMatchObject({
847
+ site: 'twitter',
848
+ source: 'local+global',
849
+ paths: { local: localSitemap, global: globalSitemap },
850
+ });
851
+ });
852
+ it('returns null when no sitemap layer exists', () => {
853
+ const report = resolveSitemapAvailabilityForUrl('https://example.com/', {
854
+ homeDir: path.join(os.tmpdir(), 'webcmd-sitemap-home'),
855
+ packageRoot: path.join(os.tmpdir(), 'webcmd-sitemap-package'),
856
+ registry: new Map(),
857
+ fileExists: () => false,
858
+ });
859
+ expect(report).toBeNull();
860
+ });
861
+ });
862
+ describe('browser verify', () => {
863
+ beforeEach(() => {
864
+ process.exitCode = undefined;
865
+ mockExecFileSync.mockReset().mockReturnValue('[]');
866
+ });
867
+ it('passes --trace through to the adapter subprocess', async () => {
868
+ const originalHome = process.env.HOME;
869
+ const originalUserProfile = process.env.USERPROFILE;
870
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-browser-verify-trace-'));
871
+ process.env.HOME = fakeHome;
872
+ process.env.USERPROFILE = fakeHome;
873
+ try {
874
+ const adapterDir = path.join(fakeHome, '.webcmd', 'clis', 'hn');
875
+ fs.mkdirSync(adapterDir, { recursive: true });
876
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
877
+ const program = createProgram('', '');
878
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture', '--trace', 'retain-on-failure']);
879
+ expect(mockExecFileSync).toHaveBeenCalledTimes(1);
880
+ const [, execArgs] = mockExecFileSync.mock.calls[0];
881
+ expect(execArgs.slice(-6)).toEqual(['hn', 'top', '--trace', 'retain-on-failure', '--format', 'json']);
882
+ }
883
+ finally {
884
+ if (originalHome === undefined)
885
+ delete process.env.HOME;
886
+ else
887
+ process.env.HOME = originalHome;
888
+ if (originalUserProfile === undefined)
889
+ delete process.env.USERPROFILE;
890
+ else
891
+ process.env.USERPROFILE = originalUserProfile;
892
+ fs.rmSync(fakeHome, { recursive: true, force: true });
893
+ }
894
+ });
895
+ it('uses --seed-args when no fixture args exist', async () => {
896
+ const originalHome = process.env.HOME;
897
+ const originalUserProfile = process.env.USERPROFILE;
898
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-browser-verify-seed-'));
899
+ process.env.HOME = fakeHome;
900
+ process.env.USERPROFILE = fakeHome;
901
+ try {
902
+ const adapterDir = path.join(fakeHome, '.webcmd', 'clis', 'hn');
903
+ fs.mkdirSync(adapterDir, { recursive: true });
904
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
905
+ const program = createProgram('', '');
906
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture', '--seed-args', 'webcmd-verify']);
907
+ expect(mockExecFileSync).toHaveBeenCalledTimes(1);
908
+ const [, execArgs] = mockExecFileSync.mock.calls[0];
909
+ expect(execArgs.slice(-5)).toEqual(['hn', 'top', 'webcmd-verify', '--format', 'json']);
910
+ }
911
+ finally {
912
+ if (originalHome === undefined)
913
+ delete process.env.HOME;
914
+ else
915
+ process.env.HOME = originalHome;
916
+ if (originalUserProfile === undefined)
917
+ delete process.env.USERPROFILE;
918
+ else
919
+ process.env.USERPROFILE = originalUserProfile;
920
+ fs.rmSync(fakeHome, { recursive: true, force: true });
921
+ }
922
+ });
923
+ it('writes --seed-args into a starter fixture', async () => {
924
+ const originalHome = process.env.HOME;
925
+ const originalUserProfile = process.env.USERPROFILE;
926
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-browser-verify-write-seed-'));
927
+ process.env.HOME = fakeHome;
928
+ process.env.USERPROFILE = fakeHome;
929
+ mockExecFileSync.mockReturnValue(JSON.stringify([{ title: 'ok' }]));
930
+ try {
931
+ const adapterDir = path.join(fakeHome, '.webcmd', 'clis', 'hn');
932
+ fs.mkdirSync(adapterDir, { recursive: true });
933
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
934
+ const program = createProgram('', '');
935
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'verify', 'hn/top', '--write-fixture', '--seed-args', 'webcmd-verify']);
936
+ const fixtureFile = path.join(fakeHome, '.webcmd', 'sites', 'hn', 'verify', 'top.json');
937
+ const fixture = JSON.parse(fs.readFileSync(fixtureFile, 'utf-8'));
938
+ expect(fixture.args).toEqual(['webcmd-verify']);
939
+ expect(fixture.expect.columns).toEqual(['title']);
940
+ }
941
+ finally {
942
+ if (originalHome === undefined)
943
+ delete process.env.HOME;
944
+ else
945
+ process.env.HOME = originalHome;
946
+ if (originalUserProfile === undefined)
947
+ delete process.env.USERPROFILE;
948
+ else
949
+ process.env.USERPROFILE = originalUserProfile;
950
+ fs.rmSync(fakeHome, { recursive: true, force: true });
951
+ }
952
+ });
953
+ it('fails before fixture handling when output row shape is not agent-native', async () => {
954
+ const originalHome = process.env.HOME;
955
+ const originalUserProfile = process.env.USERPROFILE;
956
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-browser-verify-shape-'));
957
+ process.env.HOME = fakeHome;
958
+ process.env.USERPROFILE = fakeHome;
959
+ mockExecFileSync.mockReturnValue(JSON.stringify([{ title: 'ok', author: { user_id: 'u1' } }]));
960
+ const consoleLogSpy = vi.mocked(console.log);
961
+ consoleLogSpy.mockClear();
962
+ try {
963
+ const adapterDir = path.join(fakeHome, '.webcmd', 'clis', 'hn');
964
+ fs.mkdirSync(adapterDir, { recursive: true });
965
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
966
+ const program = createProgram('', '');
967
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture']);
968
+ expect(process.exitCode).toBe(1);
969
+ const output = consoleLogSpy.mock.calls.map((args) => args.join(' ')).join('\n');
970
+ expect(output).toContain('Adapter output violates row shape conventions');
971
+ expect(output).toContain('author.user_id');
972
+ }
973
+ finally {
974
+ consoleLogSpy.mockClear();
975
+ if (originalHome === undefined)
976
+ delete process.env.HOME;
977
+ else
978
+ process.env.HOME = originalHome;
979
+ if (originalUserProfile === undefined)
980
+ delete process.env.USERPROFILE;
981
+ else
982
+ process.env.USERPROFILE = originalUserProfile;
983
+ fs.rmSync(fakeHome, { recursive: true, force: true });
984
+ }
985
+ });
986
+ });
987
+ describe('profile list', () => {
988
+ const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
989
+ beforeEach(() => {
990
+ process.exitCode = undefined;
991
+ stdoutSpy.mockClear();
992
+ vi.stubGlobal('fetch', vi.fn());
993
+ });
994
+ it('reports stale daemon instead of no profiles when status lacks profile support', async () => {
995
+ vi.mocked(fetch).mockResolvedValue({
996
+ ok: true,
997
+ json: async () => ({
998
+ ok: true,
999
+ pid: 123,
1000
+ uptime: 1,
1001
+ daemonVersion: '1.7.6',
1002
+ runtimeConnected: true,
1003
+ runtimeName: 'Cloak',
1004
+ runtimeVersion: '1.0.3',
1005
+ pending: 0,
1006
+ memoryMB: 20,
1007
+ port: 19825,
1008
+ }),
1009
+ });
1010
+ const program = createProgram('', '');
1011
+ await program.parseAsync(['node', 'webcmd', 'profile', 'list']);
1012
+ const output = stdoutSpy.mock.calls.flat().join('\n');
1013
+ expect(output).toContain('stale');
1014
+ expect(output).toContain('webcmd daemon restart');
1015
+ expect(output).not.toContain('No Cloak profiles available');
1016
+ });
1017
+ it('uses runtime profile wording when current daemon status has no profiles', async () => {
1018
+ vi.mocked(fetch).mockResolvedValue({
1019
+ ok: true,
1020
+ json: async () => ({
1021
+ ok: true,
1022
+ pid: 123,
1023
+ uptime: 1,
1024
+ daemonVersion: PKG_VERSION,
1025
+ runtimeConnected: false,
1026
+ runtimeName: 'Cloak',
1027
+ profiles: [],
1028
+ pending: 0,
1029
+ memoryMB: 20,
1030
+ port: 19825,
1031
+ }),
1032
+ });
1033
+ const program = createProgram('', '');
1034
+ await program.parseAsync(['node', 'webcmd', 'profile', 'list']);
1035
+ const output = stdoutSpy.mock.calls.flat().join('\n');
1036
+ expect(output).toContain('No Cloak runtime profiles are active');
1037
+ expect(output).toContain('Run a browser-backed command or webcmd <site> login to create one');
1038
+ expect(output).not.toContain(`Browser ${'Bridge'}`);
1039
+ expect(output).not.toContain(`Webcmd ${'extension'}`);
1040
+ expect(output).not.toContain('webcmd daemon restart');
1041
+ });
1042
+ });
1043
+ describe('browser tab targeting commands', () => {
1044
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
1045
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
1046
+ function getBrowserStateFile(cacheDir, session = 'test') {
1047
+ return path.join(cacheDir, 'browser-state', `${session}.json`);
1048
+ }
1049
+ beforeEach(() => {
1050
+ process.exitCode = undefined;
1051
+ process.env.WEBCMD_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-browser-tab-state-'));
1052
+ consoleLogSpy.mockClear();
1053
+ stderrSpy.mockClear();
1054
+ mockBrowserConnect.mockClear();
1055
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
1056
+ delete process.env.WEBCMD_WINDOW;
1057
+ mockBindTab.mockReset().mockResolvedValue({
1058
+ session: 'test',
1059
+ page: 'tab-2',
1060
+ url: 'https://user.example/inbox',
1061
+ title: 'Inbox',
1062
+ });
1063
+ mockSendCommand.mockReset().mockResolvedValue({ closed: true });
1064
+ browserState.page = {
1065
+ goto: vi.fn().mockResolvedValue(undefined),
1066
+ wait: vi.fn().mockResolvedValue(undefined),
1067
+ setActivePage: vi.fn(),
1068
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1069
+ getCurrentUrl: vi.fn().mockResolvedValue('https://one.example'),
1070
+ startNetworkCapture: vi.fn().mockResolvedValue(true),
1071
+ getCookies: vi.fn().mockResolvedValue([]),
1072
+ evaluate: vi.fn().mockResolvedValue({ ok: true }),
1073
+ snapshot: vi.fn().mockResolvedValue('snapshot'),
1074
+ tabs: vi.fn().mockResolvedValue([
1075
+ { index: 0, page: 'tab-1', url: 'https://one.example', title: 'one', active: true },
1076
+ { index: 1, page: 'tab-2', url: 'https://two.example', title: 'two', active: false },
1077
+ ]),
1078
+ selectTab: vi.fn().mockResolvedValue(undefined),
1079
+ newTab: vi.fn().mockResolvedValue('tab-3'),
1080
+ closeTab: vi.fn().mockResolvedValue(undefined),
1081
+ handleJavaScriptDialog: vi.fn().mockResolvedValue(undefined),
1082
+ frames: vi.fn().mockResolvedValue([
1083
+ { index: 0, frameId: 'frame-1', url: 'https://x.example/embed', name: 'x-embed' },
1084
+ ]),
1085
+ evaluateInFrame: vi.fn().mockResolvedValue('inside frame'),
1086
+ screenshot: vi.fn().mockResolvedValue('base64-shot'),
1087
+ annotatedScreenshot: vi.fn().mockResolvedValue('annotated-base64-shot'),
1088
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1089
+ closeWindow: vi.fn().mockResolvedValue(undefined),
1090
+ waitForDownload: vi.fn().mockResolvedValue({
1091
+ downloaded: true,
1092
+ filename: '/tmp/receipt.pdf',
1093
+ url: 'https://app.example/receipt.pdf',
1094
+ state: 'complete',
1095
+ elapsedMs: 10,
1096
+ }),
1097
+ session: 'test',
1098
+ };
1099
+ });
1100
+ function lastJsonLog() {
1101
+ const calls = consoleLogSpy.mock.calls;
1102
+ if (calls.length === 0)
1103
+ throw new Error('Expected at least one console.log call');
1104
+ const last = calls[calls.length - 1][0];
1105
+ if (typeof last !== 'string')
1106
+ throw new Error(`Expected string arg to console.log, got ${typeof last}`);
1107
+ return JSON.parse(last);
1108
+ }
1109
+ it('binds an existing Cloak tab by page id into a browser session', async () => {
1110
+ const program = createProgram('', '');
1111
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--page', 'tab-2']);
1112
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
1113
+ expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2' });
1114
+ const out = lastJsonLog();
1115
+ expect(out.session).toBe('test');
1116
+ expect(out.url).toBe('https://user.example/inbox');
1117
+ });
1118
+ it('binds an existing Cloak tab by index into a browser session', async () => {
1119
+ const program = createProgram('', '');
1120
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--index', '1']);
1121
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
1122
+ expect(mockBindTab).toHaveBeenCalledWith('test', { index: 1 });
1123
+ const out = lastJsonLog();
1124
+ expect(out.session).toBe('test');
1125
+ });
1126
+ it('requires an explicit Cloak tab target for bind', async () => {
1127
+ const program = createProgram('', '');
1128
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind']);
1129
+ expect(mockBindTab).not.toHaveBeenCalled();
1130
+ const out = lastJsonLog();
1131
+ expect(out.error.code).toBe('invalid_request');
1132
+ expect(out.error.message).toContain('exactly one Cloak tab target');
1133
+ expect(process.exitCode).toBeDefined();
1134
+ });
1135
+ it('rejects bind with both page id and index', async () => {
1136
+ const program = createProgram('', '');
1137
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--page', 'tab-2', '--index', '1']);
1138
+ expect(mockBindTab).not.toHaveBeenCalled();
1139
+ const out = lastJsonLog();
1140
+ expect(out.error.code).toBe('invalid_request');
1141
+ });
1142
+ it('requires an explicit session for browser commands', async () => {
1143
+ const program = createProgram('', '');
1144
+ // --session is now a hidden internal flag; commander no longer guards it.
1145
+ // The action body throws via getBrowserSession(), surfacing the
1146
+ // <session> positional in the error message.
1147
+ await program.parseAsync(['node', 'webcmd', 'browser', 'state']);
1148
+ expect(mockBrowserConnect).not.toHaveBeenCalled();
1149
+ expect(stderrSpy.mock.calls.flat().join('')).toContain('<session> is a required positional argument');
1150
+ });
1151
+ it('runs browser commands against an explicit session', async () => {
1152
+ const program = createProgram('', '');
1153
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state']);
1154
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1155
+ expect(browserState.page?.snapshot).toHaveBeenCalled();
1156
+ });
1157
+ it('passes browser --window through Commander options without relying on env pre-processing', async () => {
1158
+ const program = createProgram('', '');
1159
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', '--window', 'background', 'state']);
1160
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
1161
+ expect(browserState.page?.snapshot).toHaveBeenCalled();
1162
+ });
1163
+ it('passes the opt-in AX source to browser state', async () => {
1164
+ const program = createProgram('', '');
1165
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state', '--source', 'ax']);
1166
+ expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
1167
+ });
1168
+ it('prints DOM vs AX snapshot metrics without changing default state output', async () => {
1169
+ browserState.page = {
1170
+ ...browserState.page,
1171
+ snapshot: vi.fn(async (opts) => {
1172
+ if (opts?.source === 'ax') {
1173
+ return 'source: ax\n---\n[1]button "Save"\nframe "https://app.example/embed":\n [2]button "Frame Save"\n---\ninteractive: 2';
1174
+ }
1175
+ return 'URL: https://app.example\n[1] button "Save"';
1176
+ }),
1177
+ };
1178
+ const program = createProgram('', '');
1179
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state', '--compare-sources']);
1180
+ expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'dom' });
1181
+ expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
1182
+ const out = lastJsonLog();
1183
+ expect(out.url).toBe('https://one.example');
1184
+ expect(out.sources.dom).toMatchObject({ ok: true, refs: 1, frame_sections: 0 });
1185
+ expect(out.sources.ax).toMatchObject({ ok: true, refs: 2, frame_sections: 1, interactive: 2 });
1186
+ });
1187
+ it('keeps compare-sources usable when one observation backend fails', async () => {
1188
+ browserState.page = {
1189
+ ...browserState.page,
1190
+ snapshot: vi.fn(async (opts) => {
1191
+ if (opts?.source === 'ax')
1192
+ throw new Error('AX unavailable');
1193
+ return '[1] button "Save"';
1194
+ }),
1195
+ };
1196
+ const program = createProgram('', '');
1197
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state', '--compare-sources']);
1198
+ const out = lastJsonLog();
1199
+ expect(out.sources.dom).toMatchObject({ ok: true, refs: 1 });
1200
+ expect(out.sources.ax).toMatchObject({
1201
+ ok: false,
1202
+ error: { message: 'AX unavailable' },
1203
+ });
1204
+ });
1205
+ it('rejects unknown browser state sources before touching the page', async () => {
1206
+ const program = createProgram('', '');
1207
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state', '--source', 'magic']);
1208
+ expect(browserState.page?.snapshot).not.toHaveBeenCalled();
1209
+ const out = lastJsonLog();
1210
+ expect(out.error.code).toBe('invalid_source');
1211
+ expect(process.exitCode).toBeDefined();
1212
+ });
1213
+ it('captures annotated screenshots through the visual ref overlay path', async () => {
1214
+ const program = createProgram('', '');
1215
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'screenshot', '--annotate']);
1216
+ expect(browserState.page?.annotatedScreenshot).toHaveBeenCalledWith({
1217
+ fullPage: false,
1218
+ annotate: true,
1219
+ width: undefined,
1220
+ height: undefined,
1221
+ format: 'png',
1222
+ });
1223
+ expect(browserState.page?.screenshot).not.toHaveBeenCalled();
1224
+ expect(consoleLogSpy).toHaveBeenLastCalledWith('annotated-base64-shot');
1225
+ });
1226
+ it('allows history navigation in a bound session', async () => {
1227
+ browserState.page = {
1228
+ ...browserState.page,
1229
+ evaluate: vi.fn(),
1230
+ wait: vi.fn(),
1231
+ session: 'test',
1232
+ };
1233
+ const program = createProgram('', '');
1234
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'back']);
1235
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('history.back()');
1236
+ });
1237
+ it('unbinds a session through the daemon close-window command', async () => {
1238
+ const program = createProgram('', '');
1239
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'unbind']);
1240
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
1241
+ expect(mockSendCommand).toHaveBeenCalledWith('close-window', { session: 'test', surface: 'browser' });
1242
+ const out = lastJsonLog();
1243
+ expect(out).toEqual({ unbound: true, session: 'test' });
1244
+ });
1245
+ it('does not print false success when unbind fails', async () => {
1246
+ mockSendCommand.mockRejectedValueOnce(new BrowserCommandError('Session "test" is not attached to a tab.', 'bound_session_missing', 'Run bind again, then retry the browser command.'));
1247
+ const program = createProgram('', '');
1248
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'unbind']);
1249
+ const out = lastJsonLog();
1250
+ expect(out.error.code).toBe('bound_session_missing');
1251
+ expect(process.exitCode).toBeDefined();
1252
+ });
1253
+ it('accepts JavaScript dialogs through the browser dialog command', async () => {
1254
+ const program = createProgram('', '');
1255
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'dialog', 'accept', '--text', 'ok']);
1256
+ expect(browserState.page?.handleJavaScriptDialog).toHaveBeenCalledWith(true, 'ok');
1257
+ const out = lastJsonLog();
1258
+ expect(out).toEqual({ handled: true, action: 'accept', text: 'ok' });
1259
+ });
1260
+ it('emits a structured error when a browser action is blocked by a JavaScript dialog', async () => {
1261
+ browserState.page = {
1262
+ ...browserState.page,
1263
+ evaluate: vi.fn().mockRejectedValue(new Error('JavaScript dialog showing')),
1264
+ };
1265
+ const program = createProgram('', '');
1266
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', 'document.title']);
1267
+ const out = lastJsonLog();
1268
+ expect(out.error.code).toBe('javascript_dialog_open');
1269
+ expect(out.error.hint).toContain('browser dialog accept');
1270
+ expect(process.exitCode).toBeDefined();
1271
+ });
1272
+ it('binds browser commands to an explicit target tab via --tab', async () => {
1273
+ const program = createProgram('', '');
1274
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', '--tab', 'tab-2', 'document.title']);
1275
+ expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
1276
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1277
+ });
1278
+ it('rejects an explicit --tab target that is no longer in the current session', async () => {
1279
+ browserState.page = {
1280
+ setActivePage: vi.fn(),
1281
+ getActivePage: vi.fn(),
1282
+ tabs: vi.fn().mockResolvedValue([]),
1283
+ evaluate: vi.fn(),
1284
+ };
1285
+ const program = createProgram('', '');
1286
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', '--tab', 'tab-stale', 'document.title']);
1287
+ expect(process.exitCode).toBeDefined();
1288
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1289
+ expect(browserState.page?.evaluate).not.toHaveBeenCalled();
1290
+ expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
1291
+ });
1292
+ it('lists tabs with target IDs via browser tab list', async () => {
1293
+ const program = createProgram('', '');
1294
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'list']);
1295
+ expect(browserState.page?.tabs).toHaveBeenCalledTimes(1);
1296
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
1297
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-2"');
1298
+ });
1299
+ it('creates a new tab and prints its target ID', async () => {
1300
+ const program = createProgram('', '');
1301
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'new', 'https://three.example']);
1302
+ expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
1303
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-3"');
1304
+ });
1305
+ it('prints the resolved target ID when browser open creates or navigates a tab', async () => {
1306
+ const program = createProgram('', '');
1307
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'open', 'https://example.com']);
1308
+ expect(browserState.page?.goto).toHaveBeenCalledWith('https://example.com');
1309
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://one.example"');
1310
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
1311
+ });
1312
+ it('lists cross-origin frames via browser frames', async () => {
1313
+ const program = createProgram('', '');
1314
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'frames']);
1315
+ expect(browserState.page?.frames).toHaveBeenCalledTimes(1);
1316
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"frameId": "frame-1"');
1317
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://x.example/embed"');
1318
+ });
1319
+ it('routes browser eval --frame through frame-targeted evaluation', async () => {
1320
+ const program = createProgram('', '');
1321
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', '--frame', '0', 'document.title']);
1322
+ expect(browserState.page?.evaluateInFrame).toHaveBeenCalledWith('document.title', 0);
1323
+ expect(browserState.page?.evaluate).not.toHaveBeenCalled();
1324
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('inside frame');
1325
+ });
1326
+ it('does not promote a newly created tab to the persisted default target', async () => {
1327
+ const program = createProgram('', '');
1328
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'new', 'https://three.example']);
1329
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', 'document.title']);
1330
+ expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
1331
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1332
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1333
+ });
1334
+ it('persists an explicitly selected tab as the default target for later untargeted commands', async () => {
1335
+ const program = createProgram('', '');
1336
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1337
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', 'document.title']);
1338
+ expect(browserState.page?.selectTab).toHaveBeenCalledWith('tab-2');
1339
+ expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
1340
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1341
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"selected": "tab-2"');
1342
+ });
1343
+ it('clears a saved default target when it is no longer present in the current session', async () => {
1344
+ const cacheDir = String(process.env.WEBCMD_CACHE_DIR);
1345
+ const program = createProgram('', '');
1346
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1347
+ expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(true);
1348
+ browserState.page = {
1349
+ setActivePage: vi.fn(),
1350
+ getActivePage: vi.fn(),
1351
+ tabs: vi.fn().mockResolvedValue([]),
1352
+ evaluate: vi.fn().mockResolvedValue({ ok: true }),
1353
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1354
+ };
1355
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', 'document.title']);
1356
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1357
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1358
+ expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(false);
1359
+ });
1360
+ it('clears the persisted default target when that tab is closed', async () => {
1361
+ const program = createProgram('', '');
1362
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1363
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'close', 'tab-2']);
1364
+ vi.mocked(browserState.page?.setActivePage).mockClear();
1365
+ vi.mocked(browserState.page?.evaluate).mockClear();
1366
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'eval', 'document.title']);
1367
+ expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1368
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1369
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1370
+ });
1371
+ it('closes a tab by target ID', async () => {
1372
+ const program = createProgram('', '');
1373
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'close', 'tab-2']);
1374
+ expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1375
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"closed": "tab-2"');
1376
+ });
1377
+ it('rejects closing a stale tab target ID that is no longer in the current session', async () => {
1378
+ browserState.page = {
1379
+ session: 'test',
1380
+ tabs: vi.fn().mockResolvedValue([]),
1381
+ closeTab: vi.fn(),
1382
+ };
1383
+ const program = createProgram('', '');
1384
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'tab', 'close', 'tab-stale']);
1385
+ expect(process.exitCode).toBeDefined();
1386
+ expect(browserState.page?.closeTab).not.toHaveBeenCalled();
1387
+ expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
1388
+ });
1389
+ it('browser analyze merges HttpOnly cookie names from page.getCookies and drains stale capture before verdict', async () => {
1390
+ browserState.page = {
1391
+ goto: vi.fn().mockResolvedValue(undefined),
1392
+ wait: vi.fn().mockResolvedValue(undefined),
1393
+ setActivePage: vi.fn(),
1394
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1395
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1396
+ startNetworkCapture: vi.fn().mockResolvedValue(true),
1397
+ getCookies: vi.fn().mockResolvedValue([{ name: 'cf_clearance', value: 'x', domain: '.target.example' }]),
1398
+ evaluate: vi.fn().mockResolvedValue({
1399
+ cookieNames: [],
1400
+ initialState: {
1401
+ __INITIAL_STATE__: false,
1402
+ __NUXT__: false,
1403
+ __NEXT_DATA__: false,
1404
+ __APOLLO_STATE__: false,
1405
+ },
1406
+ title: 'Target',
1407
+ finalUrl: 'https://target.example/',
1408
+ }),
1409
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1410
+ readNetworkCapture: vi.fn()
1411
+ .mockResolvedValueOnce([
1412
+ {
1413
+ url: 'https://stale.example/api/old',
1414
+ method: 'GET',
1415
+ responseStatus: 200,
1416
+ responseContentType: 'application/json',
1417
+ responsePreview: '{"stale":true}',
1418
+ },
1419
+ ])
1420
+ .mockResolvedValueOnce([
1421
+ {
1422
+ url: 'https://target.example/api/items',
1423
+ method: 'GET',
1424
+ responseStatus: 200,
1425
+ responseContentType: 'application/json',
1426
+ responsePreview: '{"items":[{"title":"A","id":"1"}]}',
1427
+ },
1428
+ ]),
1429
+ };
1430
+ const program = createProgram('', '');
1431
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1432
+ const out = lastJsonLog();
1433
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1434
+ expect(out.pattern.pattern).toBe('A');
1435
+ expect(out.api_candidates[0].url).toBe('https://target.example/api/items');
1436
+ expect(out.api_candidates[0].verdict).toBe('likely_data');
1437
+ expect(out.anti_bot.evidence).toContain('cookie:cf_clearance');
1438
+ });
1439
+ it('browser analyze falls back to interceptor buffer when network capture is unsupported', async () => {
1440
+ let bufferReads = 0;
1441
+ browserState.page = {
1442
+ goto: vi.fn().mockResolvedValue(undefined),
1443
+ wait: vi.fn().mockResolvedValue(undefined),
1444
+ setActivePage: vi.fn(),
1445
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1446
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1447
+ startNetworkCapture: vi.fn().mockResolvedValue(false),
1448
+ getCookies: vi.fn().mockResolvedValue([{ name: 'cf_clearance', value: 'x', domain: '.target.example' }]),
1449
+ evaluate: vi.fn().mockImplementation(async (arg) => {
1450
+ if (typeof arg === 'string' && arg.includes('document.cookie')) {
1451
+ return {
1452
+ cookieNames: [],
1453
+ initialState: {
1454
+ __INITIAL_STATE__: false,
1455
+ __NUXT__: false,
1456
+ __NEXT_DATA__: false,
1457
+ __APOLLO_STATE__: false,
1458
+ },
1459
+ title: 'Target',
1460
+ finalUrl: 'https://target.example/',
1461
+ };
1462
+ }
1463
+ if (typeof arg === 'string' && arg.includes('window.__webcmd_net = []')) {
1464
+ bufferReads += 1;
1465
+ if (bufferReads === 1) {
1466
+ return JSON.stringify([
1467
+ {
1468
+ url: 'https://stale.example/api/old',
1469
+ method: 'GET',
1470
+ status: 200,
1471
+ size: 12,
1472
+ ct: 'application/json',
1473
+ body: { stale: true },
1474
+ },
1475
+ ]);
1476
+ }
1477
+ return JSON.stringify([
1478
+ {
1479
+ url: 'https://target.example/waf',
1480
+ method: 'GET',
1481
+ status: 403,
1482
+ size: 17,
1483
+ ct: 'text/html',
1484
+ body: 'Cloudflare Ray ID',
1485
+ },
1486
+ ]);
1487
+ }
1488
+ return undefined;
1489
+ }),
1490
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1491
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1492
+ };
1493
+ const program = createProgram('', '');
1494
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1495
+ const out = lastJsonLog();
1496
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1497
+ expect(bufferReads).toBe(2);
1498
+ expect(out.anti_bot.vendor).toBe('cloudflare');
1499
+ expect(out.anti_bot.evidence).toContain('cookie:cf_clearance');
1500
+ expect(out.anti_bot.evidence).toContain('body:https://target.example/waf');
1501
+ });
1502
+ it('browser wait xhr starts capture, injects interceptor on fallback, and ignores stale ring entries', async () => {
1503
+ browserState.page = {
1504
+ goto: vi.fn().mockResolvedValue(undefined),
1505
+ wait: vi.fn().mockResolvedValue(undefined),
1506
+ setActivePage: vi.fn(),
1507
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1508
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1509
+ startNetworkCapture: vi.fn().mockResolvedValue(false),
1510
+ evaluate: vi.fn().mockResolvedValue(undefined),
1511
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1512
+ readNetworkCapture: vi.fn()
1513
+ .mockResolvedValueOnce([
1514
+ {
1515
+ url: 'https://stale.example/api/old',
1516
+ method: 'GET',
1517
+ responseStatus: 200,
1518
+ responseContentType: 'application/json',
1519
+ responsePreview: '{"stale":true}',
1520
+ },
1521
+ ])
1522
+ .mockResolvedValueOnce([
1523
+ {
1524
+ url: 'https://target.example/api/target',
1525
+ method: 'GET',
1526
+ responseStatus: 200,
1527
+ responseContentType: 'application/json',
1528
+ responsePreview: '{"ok":true}',
1529
+ },
1530
+ ]),
1531
+ };
1532
+ const program = createProgram('', '');
1533
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1534
+ const out = lastJsonLog();
1535
+ expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1536
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith(expect.stringContaining('window.__webcmd_net'));
1537
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1538
+ expect(out.matched.url).toBe('https://target.example/api/target');
1539
+ });
1540
+ it('browser wait xhr reads interceptor buffer when network capture is unsupported', async () => {
1541
+ let bufferReads = 0;
1542
+ browserState.page = {
1543
+ goto: vi.fn().mockResolvedValue(undefined),
1544
+ wait: vi.fn().mockResolvedValue(undefined),
1545
+ setActivePage: vi.fn(),
1546
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1547
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1548
+ startNetworkCapture: vi.fn().mockResolvedValue(false),
1549
+ evaluate: vi.fn().mockImplementation(async (arg) => {
1550
+ if (typeof arg === 'string' && arg.includes('window.__webcmd_net = []')) {
1551
+ bufferReads += 1;
1552
+ if (bufferReads === 1) {
1553
+ return JSON.stringify([
1554
+ {
1555
+ url: 'https://stale.example/api/old',
1556
+ method: 'GET',
1557
+ status: 200,
1558
+ size: 12,
1559
+ ct: 'application/json',
1560
+ body: { stale: true },
1561
+ },
1562
+ ]);
1563
+ }
1564
+ return JSON.stringify([
1565
+ {
1566
+ url: 'https://target.example/api/target',
1567
+ method: 'GET',
1568
+ status: 200,
1569
+ size: 11,
1570
+ ct: 'application/json',
1571
+ body: { ok: true },
1572
+ },
1573
+ ]);
1574
+ }
1575
+ return undefined;
1576
+ }),
1577
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1578
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1579
+ };
1580
+ const program = createProgram('', '');
1581
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1582
+ const out = lastJsonLog();
1583
+ expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1584
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1585
+ expect(bufferReads).toBe(2);
1586
+ expect(out.matched.url).toBe('https://target.example/api/target');
1587
+ });
1588
+ it('browser wait download delegates to the browser runtime download observer', async () => {
1589
+ browserState.page = {
1590
+ goto: vi.fn().mockResolvedValue(undefined),
1591
+ wait: vi.fn().mockResolvedValue(undefined),
1592
+ waitForDownload: vi.fn().mockResolvedValue({
1593
+ downloaded: true,
1594
+ filename: '/tmp/receipt.pdf',
1595
+ url: 'https://app.example/receipt.pdf',
1596
+ state: 'complete',
1597
+ elapsedMs: 10,
1598
+ }),
1599
+ setActivePage: vi.fn(),
1600
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1601
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1602
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1603
+ };
1604
+ const program = createProgram('', '');
1605
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'wait', 'download', 'receipt', '--timeout', '900']);
1606
+ expect(browserState.page?.waitForDownload).toHaveBeenCalledWith('receipt', 900);
1607
+ expect(lastJsonLog()).toEqual({
1608
+ downloaded: true,
1609
+ filename: '/tmp/receipt.pdf',
1610
+ url: 'https://app.example/receipt.pdf',
1611
+ state: 'complete',
1612
+ elapsedMs: 10,
1613
+ });
1614
+ });
1615
+ it('browser wait download reports an error envelope when no matching download completes', async () => {
1616
+ browserState.page = {
1617
+ goto: vi.fn().mockResolvedValue(undefined),
1618
+ wait: vi.fn().mockResolvedValue(undefined),
1619
+ waitForDownload: vi.fn().mockResolvedValue({
1620
+ downloaded: false,
1621
+ state: 'interrupted',
1622
+ error: 'No download matched "receipt" within 900ms',
1623
+ elapsedMs: 900,
1624
+ }),
1625
+ setActivePage: vi.fn(),
1626
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1627
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1628
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1629
+ };
1630
+ const program = createProgram('', '');
1631
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'wait', 'download', 'receipt', '--timeout', '900']);
1632
+ const out = lastJsonLog();
1633
+ expect(out.error.code).toBe('download_not_seen');
1634
+ expect(out.download.elapsedMs).toBe(900);
1635
+ expect(process.exitCode).toBeDefined();
1636
+ });
1637
+ });
1638
+ describe('browser network command', () => {
1639
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
1640
+ function getNetworkCachePath(cacheDir) {
1641
+ return path.join(cacheDir, 'browser-network', 'test.json');
1642
+ }
1643
+ function getCustomNetworkCachePath(cacheDir) {
1644
+ return path.join(cacheDir, 'browser-network', 'custom.json');
1645
+ }
1646
+ function lastJsonLog() {
1647
+ const calls = consoleLogSpy.mock.calls;
1648
+ if (calls.length === 0)
1649
+ throw new Error('Expected at least one console.log call');
1650
+ const last = calls[calls.length - 1][0];
1651
+ if (typeof last !== 'string')
1652
+ throw new Error(`Expected string arg to console.log, got ${typeof last}`);
1653
+ return JSON.parse(last);
1654
+ }
1655
+ beforeEach(() => {
1656
+ process.exitCode = undefined;
1657
+ process.env.WEBCMD_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-browser-net-'));
1658
+ consoleLogSpy.mockClear();
1659
+ mockBrowserConnect.mockClear();
1660
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
1661
+ browserState.page = {
1662
+ session: 'test',
1663
+ setActivePage: vi.fn(),
1664
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1665
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
1666
+ evaluate: vi.fn().mockResolvedValue(''),
1667
+ readNetworkCapture: vi.fn().mockResolvedValue([
1668
+ {
1669
+ url: 'https://x.com/i/api/graphql/qid/UserTweets?v=1',
1670
+ method: 'GET',
1671
+ responseStatus: 200,
1672
+ responseContentType: 'application/json',
1673
+ responsePreview: JSON.stringify({ data: { user: { rest_id: '42' } } }),
1674
+ timestamp: Date.now(),
1675
+ },
1676
+ {
1677
+ url: 'https://cdn.example.com/app.js',
1678
+ method: 'GET',
1679
+ responseStatus: 200,
1680
+ responseContentType: 'application/javascript',
1681
+ responsePreview: '// js',
1682
+ },
1683
+ ]),
1684
+ };
1685
+ });
1686
+ it('emits JSON with shape previews and persists the capture to disk', async () => {
1687
+ const cacheDir = String(process.env.WEBCMD_CACHE_DIR);
1688
+ const program = createProgram('', '');
1689
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1690
+ const out = lastJsonLog();
1691
+ expect(out.count).toBe(1);
1692
+ expect(out.filtered_out).toBe(1);
1693
+ expect(out.entries[0].key).toBe('UserTweets');
1694
+ expect(out.entries[0].shape['$.data.user.rest_id']).toBe('string');
1695
+ expect(out.entries[0]).not.toHaveProperty('body');
1696
+ expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(true);
1697
+ });
1698
+ it('uses the selected browser session for network cache scope', async () => {
1699
+ const cacheDir = String(process.env.WEBCMD_CACHE_DIR);
1700
+ browserState.page = {
1701
+ ...browserState.page,
1702
+ session: 'custom',
1703
+ };
1704
+ const program = createProgram('', '');
1705
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'custom', 'network']);
1706
+ const out = lastJsonLog();
1707
+ expect(out.session).toBe('custom');
1708
+ expect(fs.existsSync(getCustomNetworkCachePath(cacheDir))).toBe(true);
1709
+ expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(false);
1710
+ });
1711
+ it('--all includes static resources that the default filter drops', async () => {
1712
+ const program = createProgram('', '');
1713
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--all']);
1714
+ const out = lastJsonLog();
1715
+ expect(out.count).toBe(2);
1716
+ expect(out.entries.map((e) => e.key)).toContain('UserTweets');
1717
+ expect(out.entries.map((e) => e.key)).toContain('GET cdn.example.com/app.js');
1718
+ });
1719
+ it('--failed and --since filter captured entries by status and time window', async () => {
1720
+ const now = Date.now();
1721
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1722
+ {
1723
+ url: 'https://api.example.com/new-fail',
1724
+ method: 'GET',
1725
+ responseStatus: 500,
1726
+ responseContentType: 'application/json',
1727
+ responsePreview: JSON.stringify({ error: true }),
1728
+ timestamp: now,
1729
+ },
1730
+ {
1731
+ url: 'https://api.example.com/old-fail',
1732
+ method: 'GET',
1733
+ responseStatus: 500,
1734
+ responseContentType: 'application/json',
1735
+ responsePreview: JSON.stringify({ error: true }),
1736
+ timestamp: now - 180_000,
1737
+ },
1738
+ {
1739
+ url: 'https://api.example.com/new-ok',
1740
+ method: 'GET',
1741
+ responseStatus: 200,
1742
+ responseContentType: 'application/json',
1743
+ responsePreview: JSON.stringify({ ok: true }),
1744
+ timestamp: now,
1745
+ },
1746
+ ]);
1747
+ const program = createProgram('', '');
1748
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--since', '120s', '--failed']);
1749
+ const out = lastJsonLog();
1750
+ expect(out.count).toBe(1);
1751
+ expect(out.entries[0].url).toBe('https://api.example.com/new-fail');
1752
+ expect(out.entries[0].timestamp).toMatch(/T/);
1753
+ });
1754
+ it('default output keeps text/javascript API responses while dropping static JS files', async () => {
1755
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1756
+ {
1757
+ url: 'https://hw.mail.163.com/js6/s?sid=abc&func=mbox:listMessages',
1758
+ method: 'POST',
1759
+ responseStatus: 200,
1760
+ responseContentType: 'text/javascript',
1761
+ responsePreview: JSON.stringify({ messages: [{ id: 'm1', subject: 'hello' }] }),
1762
+ },
1763
+ {
1764
+ url: 'https://cdn.example.com/app.js',
1765
+ method: 'GET',
1766
+ responseStatus: 200,
1767
+ responseContentType: 'application/javascript',
1768
+ responsePreview: '// js',
1769
+ },
1770
+ ]);
1771
+ const program = createProgram('', '');
1772
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1773
+ const out = lastJsonLog();
1774
+ expect(out.count).toBe(1);
1775
+ expect(out.filtered_out).toBe(1);
1776
+ expect(out.entries[0].key).toBe('POST hw.mail.163.com/js6/s');
1777
+ expect(out.entries[0].ct).toBe('text/javascript');
1778
+ expect(out.entries[0].shape['$.messages']).toBe('array(1)');
1779
+ });
1780
+ it('--raw emits full bodies inline for every entry', async () => {
1781
+ const program = createProgram('', '');
1782
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--raw']);
1783
+ const out = lastJsonLog();
1784
+ expect(out.entries[0].body).toEqual({ data: { user: { rest_id: '42' } } });
1785
+ expect(out.entries[0].timestamp).toMatch(/T/);
1786
+ });
1787
+ it('--detail <key> returns the full body for the requested entry', async () => {
1788
+ const program = createProgram('', '');
1789
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1790
+ consoleLogSpy.mockClear();
1791
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1792
+ const out = lastJsonLog();
1793
+ expect(out.key).toBe('UserTweets');
1794
+ expect(out.body).toEqual({ data: { user: { rest_id: '42' } } });
1795
+ expect(out.shape['$.data.user.rest_id']).toBe('string');
1796
+ expect(out.timestamp).toMatch(/T/);
1797
+ });
1798
+ it('--detail reports key_not_found with the list of available keys', async () => {
1799
+ const program = createProgram('', '');
1800
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1801
+ consoleLogSpy.mockClear();
1802
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--detail', 'NopeOp']);
1803
+ const out = lastJsonLog();
1804
+ expect(out.error.code).toBe('key_not_found');
1805
+ expect(out.error.available_keys).toContain('UserTweets');
1806
+ expect(process.exitCode).toBeDefined();
1807
+ });
1808
+ it('--detail reports cache_missing when no capture has been persisted yet', async () => {
1809
+ const program = createProgram('', '');
1810
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1811
+ const out = lastJsonLog();
1812
+ expect(out.error.code).toBe('cache_missing');
1813
+ expect(process.exitCode).toBeDefined();
1814
+ });
1815
+ it('emits capture_failed when readNetworkCapture throws', async () => {
1816
+ browserState.page.readNetworkCapture = vi.fn().mockRejectedValue(new Error('CDP disconnected'));
1817
+ const program = createProgram('', '');
1818
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1819
+ const out = lastJsonLog();
1820
+ expect(out.error.code).toBe('capture_failed');
1821
+ expect(out.error.message).toContain('CDP disconnected');
1822
+ expect(process.exitCode).toBeDefined();
1823
+ });
1824
+ it('surfaces cache_warning in the envelope when persistence fails', async () => {
1825
+ const cacheDir = String(process.env.WEBCMD_CACHE_DIR);
1826
+ // Pre-create the target path as a file where a directory is expected,
1827
+ // forcing the mkdir inside saveNetworkCache to throw.
1828
+ const clashDir = path.join(cacheDir, 'browser-network');
1829
+ fs.writeFileSync(clashDir, 'not-a-directory');
1830
+ const program = createProgram('', '');
1831
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1832
+ const out = lastJsonLog();
1833
+ expect(out.cache_warning).toMatch(/Could not persist capture cache/);
1834
+ expect(out.count).toBe(1);
1835
+ expect(process.exitCode).toBeUndefined();
1836
+ });
1837
+ describe('--filter', () => {
1838
+ function apiResponse(url, body) {
1839
+ return {
1840
+ url,
1841
+ method: 'GET',
1842
+ responseStatus: 200,
1843
+ responseContentType: 'application/json',
1844
+ responsePreview: JSON.stringify(body),
1845
+ };
1846
+ }
1847
+ beforeEach(() => {
1848
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1849
+ apiResponse('https://x.com/i/api/graphql/qid/UserTweets?v=1', { data: { items: [{ author: 'a', text: 't', likes: 1 }] } }),
1850
+ apiResponse('https://x.com/i/api/graphql/qid/UserProfile?v=1', { data: { user: { id: 'u1', followers: 10 } } }),
1851
+ apiResponse('https://x.com/i/api/graphql/qid/Settings?v=1', { config: { theme: 'dark' } }),
1852
+ ]);
1853
+ });
1854
+ it('narrows entries to those whose shape has ALL named fields', async () => {
1855
+ const program = createProgram('', '');
1856
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'author,text,likes']);
1857
+ const out = lastJsonLog();
1858
+ expect(out.count).toBe(1);
1859
+ expect(out.filter).toEqual(['author', 'text', 'likes']);
1860
+ expect(out.filter_dropped).toBe(2);
1861
+ expect(out.entries[0].key).toBe('UserTweets');
1862
+ });
1863
+ it('matches container segments too, not just leaf names (any-segment rule)', async () => {
1864
+ const program = createProgram('', '');
1865
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'data,items']);
1866
+ const out = lastJsonLog();
1867
+ expect(out.count).toBe(1);
1868
+ expect(out.entries[0].key).toBe('UserTweets');
1869
+ });
1870
+ it('drops entries that are missing any required field (AND semantics)', async () => {
1871
+ const program = createProgram('', '');
1872
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'author,followers']);
1873
+ const out = lastJsonLog();
1874
+ expect(out.count).toBe(0);
1875
+ expect(out.entries).toEqual([]);
1876
+ expect(out.filter).toEqual(['author', 'followers']);
1877
+ expect(out.filter_dropped).toBe(3);
1878
+ });
1879
+ it('returns empty entries (not an error) when nothing matches', async () => {
1880
+ const program = createProgram('', '');
1881
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'nonexistent_field']);
1882
+ const out = lastJsonLog();
1883
+ expect(out.count).toBe(0);
1884
+ expect(out.entries).toEqual([]);
1885
+ expect(out).not.toHaveProperty('error');
1886
+ expect(process.exitCode).toBeUndefined();
1887
+ });
1888
+ it('is case-sensitive so agents do not conflate `Id` with `id`', async () => {
1889
+ const program = createProgram('', '');
1890
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'Data']);
1891
+ const out = lastJsonLog();
1892
+ expect(out.count).toBe(0);
1893
+ });
1894
+ it('persists the full (unfiltered) capture so --detail lookups still find filtered-out keys', async () => {
1895
+ const program = createProgram('', '');
1896
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'author,text,likes']);
1897
+ consoleLogSpy.mockClear();
1898
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--detail', 'UserProfile']);
1899
+ const out = lastJsonLog();
1900
+ expect(out.key).toBe('UserProfile');
1901
+ expect(out.body).toEqual({ data: { user: { id: 'u1', followers: 10 } } });
1902
+ });
1903
+ it('composes with --raw: entries keep full bodies, filter still narrows', async () => {
1904
+ const program = createProgram('', '');
1905
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'author', '--raw']);
1906
+ const out = lastJsonLog();
1907
+ expect(out.count).toBe(1);
1908
+ expect(out.entries[0].body).toEqual({ data: { items: [{ author: 'a', text: 't', likes: 1 }] } });
1909
+ });
1910
+ it('reports invalid_filter for empty value', async () => {
1911
+ const program = createProgram('', '');
1912
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', '']);
1913
+ const out = lastJsonLog();
1914
+ expect(out.error.code).toBe('invalid_filter');
1915
+ expect(process.exitCode).toBeDefined();
1916
+ });
1917
+ it('reports invalid_filter for commas-only value', async () => {
1918
+ const program = createProgram('', '');
1919
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', ',,,']);
1920
+ const out = lastJsonLog();
1921
+ expect(out.error.code).toBe('invalid_filter');
1922
+ expect(process.exitCode).toBeDefined();
1923
+ });
1924
+ it('rejects --filter combined with --detail as invalid_args', async () => {
1925
+ const program = createProgram('', '');
1926
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--filter', 'author', '--detail', 'UserTweets']);
1927
+ const out = lastJsonLog();
1928
+ expect(out.error.code).toBe('invalid_args');
1929
+ expect(out.error.message).toContain('--filter');
1930
+ expect(out.error.message).toContain('--detail');
1931
+ expect(process.exitCode).toBeDefined();
1932
+ });
1933
+ });
1934
+ describe('body truncation signals', () => {
1935
+ it('flags body_truncated in list view when the capture layer capped the body', async () => {
1936
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1937
+ {
1938
+ url: 'https://api.example.com/huge',
1939
+ method: 'GET',
1940
+ responseStatus: 200,
1941
+ responseContentType: 'application/json',
1942
+ responsePreview: '{"data":"x"}',
1943
+ responseBodyFullSize: 99_999_999,
1944
+ responseBodyTruncated: true,
1945
+ },
1946
+ ]);
1947
+ const program = createProgram('', '');
1948
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1949
+ const out = lastJsonLog();
1950
+ expect(out.body_truncated_count).toBe(1);
1951
+ expect(out.entries[0].body_truncated).toBe(true);
1952
+ expect(out.entries[0].size).toBe(99_999_999);
1953
+ });
1954
+ it('--detail surfaces body_truncated + body_full_size when capture had to cap the body', async () => {
1955
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1956
+ {
1957
+ url: 'https://api.example.com/huge',
1958
+ method: 'GET',
1959
+ responseStatus: 200,
1960
+ responseContentType: 'application/json',
1961
+ responsePreview: 'truncated-prefix-not-valid-json',
1962
+ responseBodyFullSize: 50_000_000,
1963
+ responseBodyTruncated: true,
1964
+ },
1965
+ ]);
1966
+ const program = createProgram('', '');
1967
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1968
+ consoleLogSpy.mockClear();
1969
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--detail', 'GET api.example.com/huge']);
1970
+ const out = lastJsonLog();
1971
+ expect(out.body_truncated).toBe(true);
1972
+ expect(out.body_full_size).toBe(50_000_000);
1973
+ expect(out.body_truncation_reason).toBe('capture-limit');
1974
+ });
1975
+ it('--max-body caps the emitted body and marks body_truncation_reason = max-body', async () => {
1976
+ const longString = 'x'.repeat(5000);
1977
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1978
+ {
1979
+ url: 'https://api.example.com/plain',
1980
+ method: 'GET',
1981
+ responseStatus: 200,
1982
+ responseContentType: 'text/plain',
1983
+ responsePreview: longString,
1984
+ },
1985
+ ]);
1986
+ const program = createProgram('', '');
1987
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
1988
+ consoleLogSpy.mockClear();
1989
+ await program.parseAsync([
1990
+ 'node', 'webcmd', 'browser', '--session', 'test', 'network',
1991
+ '--detail', 'GET api.example.com/plain',
1992
+ '--max-body', '100',
1993
+ ]);
1994
+ const out = lastJsonLog();
1995
+ expect(typeof out.body).toBe('string');
1996
+ expect(out.body).toHaveLength(100);
1997
+ expect(out.body_truncated).toBe(true);
1998
+ expect(out.body_truncation_reason).toBe('max-body');
1999
+ expect(out.body_full_size).toBe(5000);
2000
+ });
2001
+ it('--max-body leaves parsed JSON bodies untouched (no mid-object cut)', async () => {
2002
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
2003
+ {
2004
+ url: 'https://api.example.com/json',
2005
+ method: 'GET',
2006
+ responseStatus: 200,
2007
+ responseContentType: 'application/json',
2008
+ responsePreview: JSON.stringify({ data: { user: { rest_id: 'u1' } } }),
2009
+ },
2010
+ ]);
2011
+ const program = createProgram('', '');
2012
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
2013
+ consoleLogSpy.mockClear();
2014
+ await program.parseAsync([
2015
+ 'node', 'webcmd', 'browser', '--session', 'test', 'network',
2016
+ '--detail', 'GET api.example.com/json',
2017
+ '--max-body', '10',
2018
+ ]);
2019
+ const out = lastJsonLog();
2020
+ // JSON body already parsed at capture time — --max-body only applies to
2021
+ // string bodies (which is where the agent-visible hazard lives).
2022
+ expect(out.body).toEqual({ data: { user: { rest_id: 'u1' } } });
2023
+ expect(out).not.toHaveProperty('body_truncated');
2024
+ });
2025
+ it('rejects non-numeric --max-body with invalid_max_body', async () => {
2026
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
2027
+ {
2028
+ url: 'https://api.example.com/x',
2029
+ method: 'GET',
2030
+ responseStatus: 200,
2031
+ responseContentType: 'application/json',
2032
+ responsePreview: '{"a":1}',
2033
+ },
2034
+ ]);
2035
+ const program = createProgram('', '');
2036
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network']);
2037
+ consoleLogSpy.mockClear();
2038
+ await program.parseAsync([
2039
+ 'node', 'webcmd', 'browser', '--session', 'test', 'network',
2040
+ '--detail', 'GET api.example.com/x',
2041
+ '--max-body', 'abc',
2042
+ ]);
2043
+ expect(lastJsonLog().error.code).toBe('invalid_max_body');
2044
+ expect(process.exitCode).toBeDefined();
2045
+ });
2046
+ it('--raw emits snake_case body_truncated / body_full_size, matching non-raw + detail', async () => {
2047
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
2048
+ {
2049
+ url: 'https://api.example.com/huge',
2050
+ method: 'GET',
2051
+ responseStatus: 200,
2052
+ responseContentType: 'application/json',
2053
+ responsePreview: 'truncated-prefix',
2054
+ responseBodyFullSize: 20_000_000,
2055
+ responseBodyTruncated: true,
2056
+ },
2057
+ ]);
2058
+ const program = createProgram('', '');
2059
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'network', '--raw']);
2060
+ const out = lastJsonLog();
2061
+ expect(out.entries).toHaveLength(1);
2062
+ const entry = out.entries[0];
2063
+ expect(entry.body_truncated).toBe(true);
2064
+ expect(entry.body_full_size).toBe(20_000_000);
2065
+ // Internal camelCase must not leak into the agent-facing envelope.
2066
+ expect(entry).not.toHaveProperty('bodyTruncated');
2067
+ expect(entry).not.toHaveProperty('bodyFullSize');
2068
+ });
2069
+ });
2070
+ });
2071
+ describe('browser console command', () => {
2072
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
2073
+ beforeEach(() => {
2074
+ process.exitCode = undefined;
2075
+ consoleLogSpy.mockClear();
2076
+ mockBrowserConnect.mockClear();
2077
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
2078
+ const now = Date.now();
2079
+ browserState.page = {
2080
+ session: 'test',
2081
+ setActivePage: vi.fn(),
2082
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
2083
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2084
+ consoleMessages: vi.fn().mockResolvedValue([
2085
+ { type: 'error', text: 'boom', timestamp: now },
2086
+ { type: 'log', text: 'ok', timestamp: now },
2087
+ { type: 'warning', text: 'old warning', timestamp: now - 180_000 },
2088
+ ]),
2089
+ };
2090
+ });
2091
+ function lastJsonLog() {
2092
+ const calls = consoleLogSpy.mock.calls;
2093
+ if (calls.length === 0)
2094
+ throw new Error('Expected at least one console.log call');
2095
+ const last = calls[calls.length - 1][0];
2096
+ if (typeof last !== 'string')
2097
+ throw new Error(`Expected string arg to console.log, got ${typeof last}`);
2098
+ return JSON.parse(last);
2099
+ }
2100
+ it('filters console messages by level and time window', async () => {
2101
+ const program = createProgram('', '');
2102
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'console', '--level', 'error', '--since', '120s']);
2103
+ const out = lastJsonLog();
2104
+ expect(out.count).toBe(1);
2105
+ expect(out.messages[0]).toMatchObject({ type: 'error', text: 'boom' });
2106
+ });
2107
+ });
2108
+ describe('browser get html command', () => {
2109
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
2110
+ function lastLogArg() {
2111
+ const calls = consoleLogSpy.mock.calls;
2112
+ if (calls.length === 0)
2113
+ throw new Error('expected console.log call');
2114
+ return calls[calls.length - 1][0];
2115
+ }
2116
+ function lastJsonLog() {
2117
+ const arg = lastLogArg();
2118
+ if (typeof arg !== 'string')
2119
+ throw new Error(`expected string arg, got ${typeof arg}`);
2120
+ return JSON.parse(arg);
2121
+ }
2122
+ beforeEach(() => {
2123
+ process.exitCode = undefined;
2124
+ process.env.WEBCMD_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-html-'));
2125
+ consoleLogSpy.mockClear();
2126
+ mockBrowserConnect.mockClear();
2127
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
2128
+ browserState.page = {
2129
+ setActivePage: vi.fn(),
2130
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
2131
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2132
+ evaluate: vi.fn(),
2133
+ };
2134
+ });
2135
+ it('returns full outerHTML by default with no truncation', async () => {
2136
+ const big = '<div>' + 'x'.repeat(100_000) + '</div>';
2137
+ browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
2138
+ const program = createProgram('', '');
2139
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html']);
2140
+ expect(lastLogArg()).toBe(big);
2141
+ });
2142
+ it('caps output with --max and prepends a visible truncation marker', async () => {
2143
+ const big = '<div>' + 'x'.repeat(500) + '</div>';
2144
+ browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
2145
+ const program = createProgram('', '');
2146
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--max', '100']);
2147
+ const out = String(lastLogArg());
2148
+ expect(out.startsWith('<!-- webcmd: truncated 100 of')).toBe(true);
2149
+ expect(out.length).toBeGreaterThan(100);
2150
+ expect(out.length).toBeLessThan(big.length);
2151
+ });
2152
+ it('rejects negative --max with invalid_max error', async () => {
2153
+ const program = createProgram('', '');
2154
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--max', '-1']);
2155
+ expect(lastJsonLog().error.code).toBe('invalid_max');
2156
+ expect(process.exitCode).toBeDefined();
2157
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2158
+ });
2159
+ it('rejects fractional --max with invalid_max error', async () => {
2160
+ const program = createProgram('', '');
2161
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--max', '1.5']);
2162
+ expect(lastJsonLog().error.code).toBe('invalid_max');
2163
+ expect(process.exitCode).toBeDefined();
2164
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2165
+ });
2166
+ it('rejects non-numeric --max (e.g. "10abc") with invalid_max error', async () => {
2167
+ const program = createProgram('', '');
2168
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--max', '10abc']);
2169
+ expect(lastJsonLog().error.code).toBe('invalid_max');
2170
+ expect(process.exitCode).toBeDefined();
2171
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2172
+ });
2173
+ it('--as json returns structured tree envelope', async () => {
2174
+ browserState.page.evaluate.mockResolvedValueOnce({
2175
+ selector: '.hero',
2176
+ matched: 1,
2177
+ tree: { tag: 'div', attrs: { class: 'hero' }, text: 'Hi', children: [] },
2178
+ });
2179
+ const program = createProgram('', '');
2180
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--selector', '.hero', '--as', 'json']);
2181
+ const out = lastJsonLog();
2182
+ expect(out.matched).toBe(1);
2183
+ expect(out.tree.tag).toBe('div');
2184
+ expect(out.tree.attrs.class).toBe('hero');
2185
+ });
2186
+ it('--as json emits selector_not_found when matched is 0', async () => {
2187
+ browserState.page.evaluate.mockResolvedValueOnce({ selector: '.missing', matched: 0, tree: null });
2188
+ const program = createProgram('', '');
2189
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--selector', '.missing', '--as', 'json']);
2190
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2191
+ expect(process.exitCode).toBeDefined();
2192
+ });
2193
+ it('raw mode emits selector_not_found when the selector matches nothing', async () => {
2194
+ browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: null });
2195
+ const program = createProgram('', '');
2196
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--selector', '.missing']);
2197
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2198
+ expect(process.exitCode).toBeDefined();
2199
+ });
2200
+ it('raw mode emits invalid_selector when the page rejects the selector syntax', async () => {
2201
+ browserState.page.evaluate.mockResolvedValueOnce({
2202
+ kind: 'invalid_selector',
2203
+ reason: "'##$@@' is not a valid selector",
2204
+ });
2205
+ const program = createProgram('', '');
2206
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--selector', '##$@@']);
2207
+ const err = lastJsonLog().error;
2208
+ expect(err.code).toBe('invalid_selector');
2209
+ expect(err.message).toContain('##$@@');
2210
+ expect(err.message).toContain('not a valid selector');
2211
+ expect(process.exitCode).toBeDefined();
2212
+ });
2213
+ it('--as json emits invalid_selector when the page rejects the selector syntax', async () => {
2214
+ browserState.page.evaluate.mockResolvedValueOnce({
2215
+ selector: '##$@@',
2216
+ invalidSelector: true,
2217
+ reason: "'##$@@' is not a valid selector",
2218
+ });
2219
+ const program = createProgram('', '');
2220
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--selector', '##$@@', '--as', 'json']);
2221
+ const err = lastJsonLog().error;
2222
+ expect(err.code).toBe('invalid_selector');
2223
+ expect(err.message).toContain('##$@@');
2224
+ expect(process.exitCode).toBeDefined();
2225
+ });
2226
+ it('rejects unknown --as format with invalid_format error', async () => {
2227
+ const program = createProgram('', '');
2228
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'html', '--as', 'yaml']);
2229
+ expect(lastJsonLog().error.code).toBe('invalid_format');
2230
+ expect(process.exitCode).toBeDefined();
2231
+ });
2232
+ });
2233
+ // Shared helper for the selector-first describe blocks below.
2234
+ // Each block spies console.log, mocks the IPage surface it touches, and
2235
+ // parses the last stringified call to inspect the JSON envelope — the
2236
+ // canonical agent-facing contract for the selector-first commands.
2237
+ function installSelectorFirstTestHarness(label, pageOverrides) {
2238
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
2239
+ function lastLogArg() {
2240
+ const calls = consoleLogSpy.mock.calls;
2241
+ if (calls.length === 0)
2242
+ throw new Error('expected console.log call');
2243
+ return calls[calls.length - 1][0];
2244
+ }
2245
+ function lastJsonLog() {
2246
+ const arg = lastLogArg();
2247
+ if (typeof arg !== 'string')
2248
+ throw new Error(`expected string arg, got ${typeof arg}`);
2249
+ return JSON.parse(arg);
2250
+ }
2251
+ beforeEach(() => {
2252
+ process.exitCode = undefined;
2253
+ process.env.WEBCMD_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), `webcmd-${label}-`));
2254
+ consoleLogSpy.mockClear();
2255
+ mockBrowserConnect.mockClear();
2256
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
2257
+ browserState.page = {
2258
+ setActivePage: vi.fn(),
2259
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
2260
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2261
+ session: 'test',
2262
+ ...pageOverrides(),
2263
+ };
2264
+ });
2265
+ return { lastJsonLog };
2266
+ }
2267
+ describe('browser find command', () => {
2268
+ const { lastJsonLog } = installSelectorFirstTestHarness('find', () => ({
2269
+ evaluate: vi.fn(),
2270
+ }));
2271
+ it('returns a {matches_n, entries} envelope for a matching selector', async () => {
2272
+ // `find` always returns numeric refs (existing on snapshot-tagged elements,
2273
+ // allocated on the spot for fresh matches) — see reviewer contract in
2274
+ // #webcmd-browser msg 52c51eb6.
2275
+ browserState.page.evaluate.mockResolvedValueOnce({
2276
+ matches_n: 2,
2277
+ entries: [
2278
+ { nth: 0, ref: 5, tag: 'button', role: '', text: 'OK', attrs: { class: 'btn' }, visible: true },
2279
+ { nth: 1, ref: 17, tag: 'button', role: '', text: 'Cancel', attrs: { class: 'btn' }, visible: true },
2280
+ ],
2281
+ });
2282
+ const program = createProgram('', '');
2283
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'find', '--css', '.btn']);
2284
+ const out = lastJsonLog();
2285
+ expect(out.matches_n).toBe(2);
2286
+ expect(out.entries).toHaveLength(2);
2287
+ expect(out.entries[0].ref).toBe(5);
2288
+ expect(out.entries[1].ref).toBe(17);
2289
+ expect(process.exitCode).toBeUndefined();
2290
+ });
2291
+ it('finds elements by semantic role/name without requiring CSS', async () => {
2292
+ browserState.page.evaluate.mockResolvedValueOnce({
2293
+ matches_n: 1,
2294
+ entries: [
2295
+ { nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2296
+ ],
2297
+ });
2298
+ const program = createProgram('', '');
2299
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'find', '--role', 'button', '--name', 'Save']);
2300
+ const js = browserState.page.evaluate.mock.calls[0][0];
2301
+ expect(js).toContain('CRITERIA');
2302
+ expect(js).toContain('function accessibleName');
2303
+ expect(lastJsonLog()).toEqual({
2304
+ matches_n: 1,
2305
+ entries: [
2306
+ { nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2307
+ ],
2308
+ });
2309
+ });
2310
+ it('forwards --limit / --text-max into the generated JS', async () => {
2311
+ browserState.page.evaluate.mockResolvedValueOnce({ matches_n: 0, entries: [] });
2312
+ const program = createProgram('', '');
2313
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'find', '--css', '.btn', '--limit', '3', '--text-max', '20']);
2314
+ const js = browserState.page.evaluate.mock.calls[0][0];
2315
+ expect(js).toContain('LIMIT = 3');
2316
+ expect(js).toContain('TEXT_MAX = 20');
2317
+ });
2318
+ it('emits invalid_selector envelope when the page rejects selector syntax', async () => {
2319
+ browserState.page.evaluate.mockResolvedValueOnce({
2320
+ error: { code: 'invalid_selector', message: 'Invalid CSS selector: ">>>"', hint: 'Check the selector syntax.' },
2321
+ });
2322
+ const program = createProgram('', '');
2323
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'find', '--css', '>>>']);
2324
+ expect(lastJsonLog().error.code).toBe('invalid_selector');
2325
+ expect(process.exitCode).toBeDefined();
2326
+ });
2327
+ it('emits selector_not_found envelope when the selector matches nothing', async () => {
2328
+ browserState.page.evaluate.mockResolvedValueOnce({
2329
+ error: { code: 'selector_not_found', message: 'CSS selector ".missing" matched 0 elements', hint: 'Use browser state to inspect the page.' },
2330
+ });
2331
+ const program = createProgram('', '');
2332
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'find', '--css', '.missing']);
2333
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2334
+ expect(process.exitCode).toBeDefined();
2335
+ });
2336
+ it('rejects missing --css with usage_error (no evaluate call)', async () => {
2337
+ const program = createProgram('', '');
2338
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'find']);
2339
+ expect(lastJsonLog().error.code).toBe('usage_error');
2340
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2341
+ expect(process.exitCode).toBeDefined();
2342
+ });
2343
+ it('rejects malformed --limit with usage_error (no evaluate call)', async () => {
2344
+ const program = createProgram('', '');
2345
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'find', '--css', '.btn', '--limit', 'abc']);
2346
+ expect(lastJsonLog().error.code).toBe('usage_error');
2347
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2348
+ expect(process.exitCode).toBeDefined();
2349
+ });
2350
+ });
2351
+ describe('browser get text/value/attributes commands', () => {
2352
+ const { lastJsonLog } = installSelectorFirstTestHarness('get-sel', () => ({
2353
+ evaluate: vi.fn(),
2354
+ }));
2355
+ it('emits {value, matches_n, match_level} envelope for a numeric ref', async () => {
2356
+ const evalMock = browserState.page.evaluate;
2357
+ // 1st call: resolveTargetJs -> { ok: true, matches_n: 1, match_level: 'exact' }
2358
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2359
+ // 2nd call: getTextResolvedJs -> the element's text
2360
+ evalMock.mockResolvedValueOnce('Hello world');
2361
+ const program = createProgram('', '');
2362
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'text', '7']);
2363
+ expect(lastJsonLog()).toEqual({ value: 'Hello world', matches_n: 1, match_level: 'exact' });
2364
+ });
2365
+ it('resolves a semantic locator to a ref before get text', async () => {
2366
+ const evalMock = browserState.page.evaluate;
2367
+ evalMock.mockResolvedValueOnce({
2368
+ matches_n: 1,
2369
+ entries: [
2370
+ { nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2371
+ ],
2372
+ });
2373
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2374
+ evalMock.mockResolvedValueOnce('Save');
2375
+ const program = createProgram('', '');
2376
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'text', '--role', 'button', '--name', 'Save']);
2377
+ expect(evalMock.mock.calls[0][0]).toContain('function accessibleName');
2378
+ expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2379
+ expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact' });
2380
+ });
2381
+ it('reports total_matches when semantic get reads the first of multiple matches', async () => {
2382
+ const evalMock = browserState.page.evaluate;
2383
+ evalMock.mockResolvedValueOnce({
2384
+ matches_n: 3,
2385
+ entries: [
2386
+ { nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2387
+ { nth: 1, ref: 13, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
2388
+ { nth: 2, ref: 14, tag: 'button', role: 'button', text: 'Save copy', attrs: {}, visible: true },
2389
+ ],
2390
+ });
2391
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2392
+ evalMock.mockResolvedValueOnce('Save');
2393
+ const program = createProgram('', '');
2394
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'text', '--role', 'button', '--name', 'Save']);
2395
+ expect(evalMock.mock.calls[0][0]).toContain('const LIMIT = 6');
2396
+ expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2397
+ expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact', total_matches: 3 });
2398
+ });
2399
+ it('reports matches_n on multi-match CSS (read path: first match wins)', async () => {
2400
+ const evalMock = browserState.page.evaluate;
2401
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 3, match_level: 'exact' });
2402
+ evalMock.mockResolvedValueOnce('first');
2403
+ const program = createProgram('', '');
2404
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'text', '.btn']);
2405
+ expect(lastJsonLog()).toEqual({ value: 'first', matches_n: 3, match_level: 'exact' });
2406
+ });
2407
+ it('parses the attributes payload back into a real object', async () => {
2408
+ const evalMock = browserState.page.evaluate;
2409
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2410
+ // getAttributesResolvedJs returns a JSON-encoded string — the CLI must parse it
2411
+ evalMock.mockResolvedValueOnce(JSON.stringify({ id: 'nav', class: 'hero' }));
2412
+ const program = createProgram('', '');
2413
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'attributes', '#nav']);
2414
+ const out = lastJsonLog();
2415
+ expect(out.matches_n).toBe(1);
2416
+ expect(out.match_level).toBe('exact');
2417
+ expect(out.value).toEqual({ id: 'nav', class: 'hero' });
2418
+ });
2419
+ it('propagates selector_not_found from the resolver as an error envelope', async () => {
2420
+ browserState.page.evaluate.mockResolvedValueOnce({
2421
+ ok: false,
2422
+ code: 'selector_not_found',
2423
+ message: 'CSS selector ".missing" matched 0 elements',
2424
+ hint: 'Try a less specific selector.',
2425
+ });
2426
+ const program = createProgram('', '');
2427
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'text', '.missing']);
2428
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2429
+ expect(process.exitCode).toBeDefined();
2430
+ });
2431
+ it('forwards --nth into the resolver opts and reports matches_n', async () => {
2432
+ const evalMock = browserState.page.evaluate;
2433
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 4, match_level: 'exact' });
2434
+ evalMock.mockResolvedValueOnce('second');
2435
+ const program = createProgram('', '');
2436
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'value', '.btn', '--nth', '1']);
2437
+ const resolveJs = evalMock.mock.calls[0][0];
2438
+ // resolveTargetJs embeds nth as a raw number literal; look for the binding
2439
+ expect(resolveJs).toContain('const nth = 1');
2440
+ expect(lastJsonLog()).toEqual({ value: 'second', matches_n: 4, match_level: 'exact' });
2441
+ });
2442
+ it('rejects malformed --nth with usage_error before touching the page', async () => {
2443
+ const program = createProgram('', '');
2444
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'get', 'text', '.btn', '--nth', 'abc']);
2445
+ expect(lastJsonLog().error.code).toBe('usage_error');
2446
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2447
+ expect(process.exitCode).toBeDefined();
2448
+ });
2449
+ });
2450
+ describe('browser click/type commands', () => {
2451
+ const { lastJsonLog } = installSelectorFirstTestHarness('click-type', () => ({
2452
+ evaluate: vi.fn().mockResolvedValue(false),
2453
+ click: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2454
+ dblClick: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2455
+ hover: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2456
+ focus: vi.fn().mockResolvedValue({ focused: true, matches_n: 1, match_level: 'exact' }),
2457
+ setChecked: vi.fn().mockResolvedValue({ checked: true, changed: true, matches_n: 1, match_level: 'exact', kind: 'checkbox' }),
2458
+ uploadFiles: vi.fn().mockResolvedValue({
2459
+ uploaded: true,
2460
+ files: 1,
2461
+ file_names: ['receipt.pdf'],
2462
+ target: '#file',
2463
+ matches_n: 1,
2464
+ match_level: 'exact',
2465
+ multiple: false,
2466
+ }),
2467
+ drag: vi.fn().mockResolvedValue({
2468
+ dragged: true,
2469
+ source: '#card',
2470
+ target: '#lane',
2471
+ source_matches_n: 1,
2472
+ target_matches_n: 1,
2473
+ source_match_level: 'exact',
2474
+ target_match_level: 'exact',
2475
+ }),
2476
+ typeText: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2477
+ fillText: vi.fn().mockResolvedValue({
2478
+ filled: true,
2479
+ verified: true,
2480
+ expected: '',
2481
+ actual: '',
2482
+ length: 0,
2483
+ matches_n: 1,
2484
+ match_level: 'exact',
2485
+ mode: 'input',
2486
+ }),
2487
+ wait: vi.fn().mockResolvedValue(undefined),
2488
+ }));
2489
+ it('emits {clicked, target, matches_n, match_level} on success', async () => {
2490
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2491
+ const program = createProgram('', '');
2492
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '#save']);
2493
+ expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
2494
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
2495
+ });
2496
+ it('clicks a unique semantic locator without a prior state call', async () => {
2497
+ browserState.page.evaluate.mockResolvedValueOnce({
2498
+ matches_n: 1,
2499
+ entries: [
2500
+ { nth: 0, ref: 23, tag: 'button', role: 'button', text: 'Submit', attrs: {}, visible: true },
2501
+ ],
2502
+ });
2503
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2504
+ const program = createProgram('', '');
2505
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '--role', 'button', '--name', 'Submit']);
2506
+ expect(browserState.page.click).toHaveBeenCalledWith('23', {});
2507
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '23', matches_n: 1, match_level: 'exact' });
2508
+ });
2509
+ it('rejects ambiguous semantic locators before write actions', async () => {
2510
+ browserState.page.evaluate.mockResolvedValueOnce({
2511
+ matches_n: 2,
2512
+ entries: [
2513
+ { nth: 0, ref: 1, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2514
+ { nth: 1, ref: 2, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
2515
+ ],
2516
+ });
2517
+ const program = createProgram('', '');
2518
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '--role', 'button', '--name', 'Save']);
2519
+ const err = lastJsonLog().error;
2520
+ expect(err.code).toBe('semantic_ambiguous');
2521
+ expect(err.matches_n).toBe(2);
2522
+ expect(browserState.page.click).not.toHaveBeenCalled();
2523
+ expect(process.exitCode).toBeDefined();
2524
+ });
2525
+ it('hover: resolves a semantic locator before moving the mouse', async () => {
2526
+ browserState.page.evaluate.mockResolvedValueOnce({
2527
+ matches_n: 1,
2528
+ entries: [
2529
+ { nth: 0, ref: 31, tag: 'button', role: 'button', text: 'Settings', attrs: {}, visible: true },
2530
+ ],
2531
+ });
2532
+ const program = createProgram('', '');
2533
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'hover', '--role', 'button', '--name', 'Settings']);
2534
+ expect(browserState.page.hover).toHaveBeenCalledWith('31', {});
2535
+ expect(lastJsonLog()).toEqual({ hovered: true, target: '31', matches_n: 1, match_level: 'exact' });
2536
+ });
2537
+ it('check: resolves a semantic locator before setting checked state', async () => {
2538
+ browserState.page.evaluate.mockResolvedValueOnce({
2539
+ matches_n: 1,
2540
+ entries: [
2541
+ { nth: 0, ref: 32, tag: 'input', role: 'checkbox', text: 'Accept', attrs: {}, visible: true },
2542
+ ],
2543
+ });
2544
+ browserState.page.setChecked.mockResolvedValueOnce({ checked: true, changed: false, matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2545
+ const program = createProgram('', '');
2546
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'check', '--role', 'checkbox', '--name', 'Accept']);
2547
+ expect(browserState.page.setChecked).toHaveBeenCalledWith('32', true, {});
2548
+ expect(lastJsonLog()).toEqual({ checked: true, changed: false, target: '32', matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2549
+ });
2550
+ it('upload: treats the first positional as a file when using semantic locator flags', async () => {
2551
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-upload-semantic-'));
2552
+ const file = path.join(dir, 'receipt.pdf');
2553
+ fs.writeFileSync(file, 'pdf');
2554
+ browserState.page.evaluate.mockResolvedValueOnce({
2555
+ matches_n: 1,
2556
+ entries: [
2557
+ { nth: 0, ref: 33, tag: 'input', role: 'button', text: 'Upload receipt', attrs: {}, visible: true },
2558
+ ],
2559
+ });
2560
+ browserState.page.uploadFiles.mockResolvedValueOnce({
2561
+ uploaded: true,
2562
+ files: 1,
2563
+ file_names: ['receipt.pdf'],
2564
+ target: '33',
2565
+ matches_n: 1,
2566
+ match_level: 'exact',
2567
+ multiple: false,
2568
+ });
2569
+ const program = createProgram('', '');
2570
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'upload', '--role', 'button', '--name', 'Upload receipt', file]);
2571
+ expect(browserState.page.uploadFiles).toHaveBeenCalledWith('33', [file], {});
2572
+ expect(lastJsonLog()).toMatchObject({ uploaded: true, target: '33', files: 1 });
2573
+ });
2574
+ it('type: treats the first positional as text when using semantic locator flags', async () => {
2575
+ browserState.page.evaluate
2576
+ .mockResolvedValueOnce({
2577
+ matches_n: 1,
2578
+ entries: [
2579
+ { nth: 0, ref: 34, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
2580
+ ],
2581
+ })
2582
+ .mockResolvedValueOnce(false);
2583
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2584
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2585
+ const program = createProgram('', '');
2586
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'type', '--label', 'Email', 'me@example.com']);
2587
+ expect(browserState.page.click).toHaveBeenCalledWith('34', {});
2588
+ expect(browserState.page.typeText).toHaveBeenCalledWith('34', 'me@example.com', {});
2589
+ expect(lastJsonLog()).toMatchObject({ typed: true, target: '34', text: 'me@example.com' });
2590
+ });
2591
+ it('fill: treats the first positional as text when using semantic locator flags', async () => {
2592
+ browserState.page.evaluate.mockResolvedValueOnce({
2593
+ matches_n: 1,
2594
+ entries: [
2595
+ { nth: 0, ref: 35, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
2596
+ ],
2597
+ });
2598
+ browserState.page.fillText.mockResolvedValueOnce({
2599
+ filled: true,
2600
+ verified: true,
2601
+ expected: 'me@example.com',
2602
+ actual: 'me@example.com',
2603
+ length: 14,
2604
+ matches_n: 1,
2605
+ match_level: 'exact',
2606
+ });
2607
+ const program = createProgram('', '');
2608
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'fill', '--label', 'Email', 'me@example.com']);
2609
+ expect(browserState.page.fillText).toHaveBeenCalledWith('35', 'me@example.com', {});
2610
+ expect(lastJsonLog()).toMatchObject({ filled: true, verified: true, target: '35', text: 'me@example.com' });
2611
+ });
2612
+ it('drag: resolves source and target from prefixed semantic locators', async () => {
2613
+ browserState.page.evaluate
2614
+ .mockResolvedValueOnce({
2615
+ matches_n: 1,
2616
+ entries: [
2617
+ { nth: 0, ref: 40, tag: 'div', role: 'button', text: 'Card A', attrs: {}, visible: true },
2618
+ ],
2619
+ })
2620
+ .mockResolvedValueOnce({
2621
+ matches_n: 1,
2622
+ entries: [
2623
+ { nth: 0, ref: 41, tag: 'div', role: 'region', text: 'Done', attrs: {}, visible: true },
2624
+ ],
2625
+ });
2626
+ browserState.page.drag.mockResolvedValueOnce({
2627
+ dragged: true,
2628
+ source: '40',
2629
+ target: '41',
2630
+ source_matches_n: 1,
2631
+ target_matches_n: 1,
2632
+ source_match_level: 'exact',
2633
+ target_match_level: 'exact',
2634
+ });
2635
+ const program = createProgram('', '');
2636
+ await program.parseAsync([
2637
+ 'node',
2638
+ 'webcmd',
2639
+ 'browser',
2640
+ '--session',
2641
+ 'test',
2642
+ 'drag',
2643
+ '--from-role',
2644
+ 'button',
2645
+ '--from-name',
2646
+ 'Card A',
2647
+ '--to-role',
2648
+ 'region',
2649
+ '--to-name',
2650
+ 'Done',
2651
+ ]);
2652
+ expect(browserState.page.drag).toHaveBeenCalledWith('40', '41', { from: {}, to: {} });
2653
+ expect(lastJsonLog()).toMatchObject({ dragged: true, source: '40', target: '41' });
2654
+ });
2655
+ it('surfaces match_level=stable when resolver falls back to fingerprint match', async () => {
2656
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'stable' });
2657
+ const program = createProgram('', '');
2658
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '7']);
2659
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '7', matches_n: 1, match_level: 'stable' });
2660
+ });
2661
+ it('forwards --nth as ResolveOptions.nth to page.click', async () => {
2662
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 3, match_level: 'exact' });
2663
+ const program = createProgram('', '');
2664
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '.btn', '--nth', '2']);
2665
+ expect(browserState.page.click).toHaveBeenCalledWith('.btn', { nth: 2 });
2666
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '.btn', matches_n: 3, match_level: 'exact' });
2667
+ });
2668
+ it('surfaces selector_ambiguous from page.click as an error envelope', async () => {
2669
+ browserState.page.click.mockRejectedValueOnce(new TargetError({
2670
+ code: 'selector_ambiguous',
2671
+ message: 'CSS selector ".btn" matched 3 elements; clicks require a unique target.',
2672
+ hint: 'Pass --nth <n> to pick one (0-based).',
2673
+ matches_n: 3,
2674
+ }));
2675
+ const program = createProgram('', '');
2676
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '.btn']);
2677
+ const err = lastJsonLog().error;
2678
+ expect(err.code).toBe('selector_ambiguous');
2679
+ expect(err.matches_n).toBe(3);
2680
+ expect(process.exitCode).toBeDefined();
2681
+ });
2682
+ it('surfaces selector_nth_out_of_range from page.click as an error envelope', async () => {
2683
+ browserState.page.click.mockRejectedValueOnce(new TargetError({
2684
+ code: 'selector_nth_out_of_range',
2685
+ message: '--nth 99 is out of range for CSS selector ".btn" (matches_n=3).',
2686
+ hint: 'Pick an index in [0, 2].',
2687
+ matches_n: 3,
2688
+ }));
2689
+ const program = createProgram('', '');
2690
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '.btn', '--nth', '99']);
2691
+ expect(lastJsonLog().error.code).toBe('selector_nth_out_of_range');
2692
+ expect(process.exitCode).toBeDefined();
2693
+ });
2694
+ it('rejects malformed --nth on click with usage_error before touching the page', async () => {
2695
+ const program = createProgram('', '');
2696
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '.btn', '--nth', 'abc']);
2697
+ expect(lastJsonLog().error.code).toBe('usage_error');
2698
+ expect(browserState.page.click).not.toHaveBeenCalled();
2699
+ expect(process.exitCode).toBeDefined();
2700
+ });
2701
+ it('hover: delegates to page.hover and emits a structured envelope', async () => {
2702
+ browserState.page.hover.mockResolvedValueOnce({ matches_n: 2, match_level: 'exact' });
2703
+ const program = createProgram('', '');
2704
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'hover', '.menu', '--nth', '1']);
2705
+ expect(browserState.page.hover).toHaveBeenCalledWith('.menu', { nth: 1 });
2706
+ expect(lastJsonLog()).toEqual({ hovered: true, target: '.menu', matches_n: 2, match_level: 'exact' });
2707
+ });
2708
+ it('focus: delegates to page.focus and reports whether the element took focus', async () => {
2709
+ browserState.page.focus.mockResolvedValueOnce({ focused: true, matches_n: 1, match_level: 'stable' });
2710
+ const program = createProgram('', '');
2711
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'focus', '7']);
2712
+ expect(browserState.page.focus).toHaveBeenCalledWith('7', {});
2713
+ expect(lastJsonLog()).toEqual({ focused: true, target: '7', matches_n: 1, match_level: 'stable' });
2714
+ });
2715
+ it('dblclick: delegates to page.dblClick and emits a structured envelope', async () => {
2716
+ browserState.page.dblClick.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2717
+ const program = createProgram('', '');
2718
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'dblclick', '#row']);
2719
+ expect(browserState.page.dblClick).toHaveBeenCalledWith('#row', {});
2720
+ expect(lastJsonLog()).toEqual({ dblclicked: true, target: '#row', matches_n: 1, match_level: 'exact' });
2721
+ });
2722
+ it('check: ensures target is checked through page.setChecked', async () => {
2723
+ browserState.page.setChecked.mockResolvedValueOnce({
2724
+ checked: true,
2725
+ changed: true,
2726
+ matches_n: 2,
2727
+ match_level: 'exact',
2728
+ kind: 'checkbox',
2729
+ });
2730
+ const program = createProgram('', '');
2731
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'check', '.todo', '--nth', '1']);
2732
+ expect(browserState.page.setChecked).toHaveBeenCalledWith('.todo', true, { nth: 1 });
2733
+ expect(lastJsonLog()).toEqual({ checked: true, changed: true, target: '.todo', matches_n: 2, match_level: 'exact', kind: 'checkbox' });
2734
+ });
2735
+ it('uncheck: ensures target is unchecked through page.setChecked', async () => {
2736
+ browserState.page.setChecked.mockResolvedValueOnce({
2737
+ checked: false,
2738
+ changed: false,
2739
+ matches_n: 1,
2740
+ match_level: 'stable',
2741
+ kind: 'checkbox',
2742
+ });
2743
+ const program = createProgram('', '');
2744
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'uncheck', '#subscribe']);
2745
+ expect(browserState.page.setChecked).toHaveBeenCalledWith('#subscribe', false, {});
2746
+ expect(lastJsonLog()).toEqual({ checked: false, changed: false, target: '#subscribe', matches_n: 1, match_level: 'stable', kind: 'checkbox' });
2747
+ });
2748
+ it('upload: validates local files and delegates to page.uploadFiles', async () => {
2749
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-upload-'));
2750
+ const file = path.join(dir, 'receipt.pdf');
2751
+ fs.writeFileSync(file, 'pdf');
2752
+ browserState.page.uploadFiles.mockResolvedValueOnce({
2753
+ uploaded: true,
2754
+ files: 1,
2755
+ file_names: ['receipt.pdf'],
2756
+ target: '#file',
2757
+ matches_n: 1,
2758
+ match_level: 'exact',
2759
+ multiple: false,
2760
+ });
2761
+ const program = createProgram('', '');
2762
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'upload', '#file', file]);
2763
+ expect(browserState.page.uploadFiles).toHaveBeenCalledWith('#file', [file], {});
2764
+ expect(lastJsonLog()).toEqual({
2765
+ uploaded: true,
2766
+ files: 1,
2767
+ file_names: ['receipt.pdf'],
2768
+ target: '#file',
2769
+ matches_n: 1,
2770
+ match_level: 'exact',
2771
+ multiple: false,
2772
+ });
2773
+ });
2774
+ it('upload: rejects missing files before touching the page', async () => {
2775
+ const program = createProgram('', '');
2776
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'upload', '#file', '/tmp/webcmd-missing-file']);
2777
+ expect(lastJsonLog().error.code).toBe('file_not_found');
2778
+ expect(browserState.page.uploadFiles).not.toHaveBeenCalled();
2779
+ expect(process.exitCode).toBeDefined();
2780
+ });
2781
+ it('drag: delegates to page.drag and forwards source/target nth options', async () => {
2782
+ browserState.page.drag.mockResolvedValueOnce({
2783
+ dragged: true,
2784
+ source: '.card',
2785
+ target: '.lane',
2786
+ source_matches_n: 3,
2787
+ target_matches_n: 2,
2788
+ source_match_level: 'exact',
2789
+ target_match_level: 'stable',
2790
+ });
2791
+ const program = createProgram('', '');
2792
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'drag', '.card', '.lane', '--from-nth', '2', '--to-nth', '1']);
2793
+ expect(browserState.page.drag).toHaveBeenCalledWith('.card', '.lane', { from: { nth: 2 }, to: { nth: 1 } });
2794
+ expect(lastJsonLog()).toEqual({
2795
+ dragged: true,
2796
+ source: '.card',
2797
+ target: '.lane',
2798
+ source_matches_n: 3,
2799
+ target_matches_n: 2,
2800
+ source_match_level: 'exact',
2801
+ target_match_level: 'stable',
2802
+ });
2803
+ });
2804
+ it('drag: rejects malformed --from-nth before touching the page', async () => {
2805
+ const program = createProgram('', '');
2806
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'drag', '.card', '.lane', '--from-nth', 'abc']);
2807
+ expect(lastJsonLog().error.code).toBe('usage_error');
2808
+ expect(browserState.page.drag).not.toHaveBeenCalled();
2809
+ expect(process.exitCode).toBeDefined();
2810
+ });
2811
+ it('type: clicks, waits, then typeText — emits {typed, text, target, matches_n, match_level, autocomplete}', async () => {
2812
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2813
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2814
+ browserState.page.evaluate.mockResolvedValueOnce(false); // isAutocomplete
2815
+ const program = createProgram('', '');
2816
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'type', '#q', 'hello']);
2817
+ expect(browserState.page.click).toHaveBeenCalledWith('#q', {});
2818
+ expect(browserState.page.wait).toHaveBeenCalledWith(0.3);
2819
+ expect(browserState.page.typeText).toHaveBeenCalledWith('#q', 'hello', {});
2820
+ expect(lastJsonLog()).toEqual({
2821
+ typed: true, text: 'hello', target: '#q', matches_n: 1, match_level: 'exact', autocomplete: false,
2822
+ });
2823
+ });
2824
+ it('type: waits an extra 0.4s when the input reports autocomplete=true', async () => {
2825
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2826
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2827
+ browserState.page.evaluate.mockResolvedValueOnce(true);
2828
+ const program = createProgram('', '');
2829
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'type', '#q', 'hi']);
2830
+ const waitCalls = browserState.page.wait.mock.calls;
2831
+ expect(waitCalls).toContainEqual([0.3]);
2832
+ expect(waitCalls).toContainEqual([0.4]);
2833
+ expect(lastJsonLog().autocomplete).toBe(true);
2834
+ expect(lastJsonLog().match_level).toBe('exact');
2835
+ });
2836
+ it('type: surfaces match_level=reidentified when ref had to be reidentified by fingerprint', async () => {
2837
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'reidentified' });
2838
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'reidentified' });
2839
+ browserState.page.evaluate.mockResolvedValueOnce(false);
2840
+ const program = createProgram('', '');
2841
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'type', '9', 'hi']);
2842
+ // The typeText call is the authoritative match_level source for the `type` envelope.
2843
+ expect(lastJsonLog().match_level).toBe('reidentified');
2844
+ });
2845
+ it('type: forwards --nth to both click and typeText', async () => {
2846
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2847
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2848
+ const program = createProgram('', '');
2849
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'type', '.field', 'x', '--nth', '3']);
2850
+ expect(browserState.page.click).toHaveBeenCalledWith('.field', { nth: 3 });
2851
+ expect(browserState.page.typeText).toHaveBeenCalledWith('.field', 'x', { nth: 3 });
2852
+ });
2853
+ it('fill: delegates exact raw text to page.fillText and emits verification details', async () => {
2854
+ browserState.page.fillText.mockResolvedValueOnce({
2855
+ filled: true,
2856
+ verified: true,
2857
+ expected: 'line1\\n/ / raw',
2858
+ actual: 'line1\\n/ / raw',
2859
+ length: 14,
2860
+ matches_n: 1,
2861
+ match_level: 'exact',
2862
+ mode: 'textarea',
2863
+ });
2864
+ const program = createProgram('', '');
2865
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'fill', '#msg', 'line1\\n/ / raw']);
2866
+ expect(browserState.page.fillText).toHaveBeenCalledWith('#msg', 'line1\\n/ / raw', {});
2867
+ expect(lastJsonLog()).toEqual({
2868
+ filled: true,
2869
+ verified: true,
2870
+ target: '#msg',
2871
+ text: 'line1\\n/ / raw',
2872
+ actual: 'line1\\n/ / raw',
2873
+ length: 14,
2874
+ matches_n: 1,
2875
+ match_level: 'exact',
2876
+ mode: 'textarea',
2877
+ });
2878
+ expect(process.exitCode).toBeUndefined();
2879
+ });
2880
+ it('fill: sets a non-zero exit code when verification fails', async () => {
2881
+ browserState.page.fillText.mockResolvedValueOnce({
2882
+ filled: true,
2883
+ verified: false,
2884
+ expected: 'expected',
2885
+ actual: 'actual',
2886
+ length: 6,
2887
+ matches_n: 1,
2888
+ match_level: 'exact',
2889
+ });
2890
+ const program = createProgram('', '');
2891
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'fill', '#msg', 'expected']);
2892
+ expect(lastJsonLog()).toEqual({
2893
+ filled: true,
2894
+ verified: false,
2895
+ target: '#msg',
2896
+ text: 'expected',
2897
+ actual: 'actual',
2898
+ length: 6,
2899
+ matches_n: 1,
2900
+ match_level: 'exact',
2901
+ });
2902
+ expect(process.exitCode).toBeDefined();
2903
+ });
2904
+ it('fill: forwards --nth to page.fillText', async () => {
2905
+ const program = createProgram('', '');
2906
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'fill', '.field', 'x', '--nth', '2']);
2907
+ expect(browserState.page.fillText).toHaveBeenCalledWith('.field', 'x', { nth: 2 });
2908
+ });
2909
+ });
2910
+ describe('browser select command', () => {
2911
+ const { lastJsonLog } = installSelectorFirstTestHarness('select', () => ({
2912
+ evaluate: vi.fn(),
2913
+ }));
2914
+ it('emits {selected, target, matches_n, match_level} on success', async () => {
2915
+ const evalMock = browserState.page.evaluate;
2916
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2917
+ evalMock.mockResolvedValueOnce({ selected: 'US' });
2918
+ const program = createProgram('', '');
2919
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'select', '#country', 'US']);
2920
+ expect(lastJsonLog()).toEqual({ selected: 'US', target: '#country', matches_n: 1, match_level: 'exact' });
2921
+ });
2922
+ it('maps "Not a <select>" to a not_a_select error envelope', async () => {
2923
+ const evalMock = browserState.page.evaluate;
2924
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2925
+ evalMock.mockResolvedValueOnce({ error: 'Not a <select>' });
2926
+ const program = createProgram('', '');
2927
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'select', '#not-select', 'US']);
2928
+ const err = lastJsonLog().error;
2929
+ expect(err.code).toBe('not_a_select');
2930
+ expect(err.matches_n).toBe(1);
2931
+ expect(process.exitCode).toBeDefined();
2932
+ });
2933
+ it('maps missing-option failures to an option_not_found envelope with available list', async () => {
2934
+ const evalMock = browserState.page.evaluate;
2935
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2936
+ evalMock.mockResolvedValueOnce({ error: 'Option "XX" not found', available: ['US', 'CA'] });
2937
+ const program = createProgram('', '');
2938
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'select', '#country', 'XX']);
2939
+ const err = lastJsonLog().error;
2940
+ expect(err.code).toBe('option_not_found');
2941
+ expect(err.available).toEqual(['US', 'CA']);
2942
+ expect(process.exitCode).toBeDefined();
2943
+ });
2944
+ it('select: treats the first positional as option when using semantic locator flags', async () => {
2945
+ const evalMock = browserState.page.evaluate;
2946
+ evalMock
2947
+ .mockResolvedValueOnce({
2948
+ matches_n: 1,
2949
+ entries: [
2950
+ { nth: 0, ref: 36, tag: 'select', role: 'combobox', text: 'Country', attrs: {}, visible: true },
2951
+ ],
2952
+ })
2953
+ .mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' })
2954
+ .mockResolvedValueOnce({ selected: 'Uruguay' });
2955
+ const program = createProgram('', '');
2956
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'select', '--label', 'Country', 'Uruguay']);
2957
+ expect(lastJsonLog()).toEqual({ selected: 'Uruguay', target: '36', matches_n: 1, match_level: 'exact' });
2958
+ });
2959
+ it('surfaces selector_ambiguous from the resolver before calling selectResolvedJs', async () => {
2960
+ browserState.page.evaluate.mockResolvedValueOnce({
2961
+ ok: false,
2962
+ code: 'selector_ambiguous',
2963
+ message: 'CSS selector ".dropdown" matched 2 elements.',
2964
+ hint: 'Pass --nth <n>.',
2965
+ matches_n: 2,
2966
+ });
2967
+ const program = createProgram('', '');
2968
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'select', '.dropdown', 'US']);
2969
+ expect(lastJsonLog().error.code).toBe('selector_ambiguous');
2970
+ // The select payload JS must not fire when resolution fails
2971
+ expect(browserState.page.evaluate.mock.calls).toHaveLength(1);
2972
+ expect(process.exitCode).toBeDefined();
2973
+ });
2974
+ });
2975
+ describe('findPackageRoot', () => {
2976
+ it('walks up from dist/src to the package root', () => {
2977
+ const packageRoot = path.join('repo-root');
2978
+ const cliFile = path.join(packageRoot, 'dist', 'src', 'cli.js');
2979
+ const exists = new Set([
2980
+ path.join(packageRoot, 'package.json'),
2981
+ ]);
2982
+ expect(findPackageRoot(cliFile, (candidate) => exists.has(candidate))).toBe(packageRoot);
2983
+ });
2984
+ it('walks up from src to the package root', () => {
2985
+ const packageRoot = path.join('repo-root');
2986
+ const cliFile = path.join(packageRoot, 'src', 'cli.ts');
2987
+ const exists = new Set([
2988
+ path.join(packageRoot, 'package.json'),
2989
+ ]);
2990
+ expect(findPackageRoot(cliFile, (candidate) => exists.has(candidate))).toBe(packageRoot);
2991
+ });
2992
+ });
2993
+ describe('normalizeVerifyRows', () => {
2994
+ it('returns an empty array for null / primitives', () => {
2995
+ expect(normalizeVerifyRows(null)).toEqual([]);
2996
+ expect(normalizeVerifyRows(undefined)).toEqual([]);
2997
+ expect(normalizeVerifyRows('hello')).toEqual([]);
2998
+ });
2999
+ it('passes through array-of-objects', () => {
3000
+ const rows = [{ a: 1 }, { a: 2 }];
3001
+ expect(normalizeVerifyRows(rows)).toEqual(rows);
3002
+ });
3003
+ it('wraps array-of-primitives as { value } rows', () => {
3004
+ expect(normalizeVerifyRows([1, 'two', null])).toEqual([
3005
+ { value: 1 }, { value: 'two' }, { value: null },
3006
+ ]);
3007
+ });
3008
+ it('unwraps common envelope shapes', () => {
3009
+ expect(normalizeVerifyRows({ rows: [{ a: 1 }] })).toEqual([{ a: 1 }]);
3010
+ expect(normalizeVerifyRows({ items: [{ b: 2 }] })).toEqual([{ b: 2 }]);
3011
+ expect(normalizeVerifyRows({ data: [{ c: 3 }] })).toEqual([{ c: 3 }]);
3012
+ expect(normalizeVerifyRows({ results: [{ d: 4 }] })).toEqual([{ d: 4 }]);
3013
+ });
3014
+ it('wraps a single object as a one-row array', () => {
3015
+ expect(normalizeVerifyRows({ ok: true })).toEqual([{ ok: true }]);
3016
+ });
3017
+ });
3018
+ describe('renderVerifyPreview', () => {
3019
+ it('emits a placeholder for empty rows', () => {
3020
+ expect(renderVerifyPreview([])).toContain('no rows');
3021
+ });
3022
+ it('prints column headers followed by row cells', () => {
3023
+ const out = renderVerifyPreview([{ a: 'x', b: 1 }, { a: 'y', b: 2 }]);
3024
+ const lines = out.split('\n');
3025
+ expect(lines[0]).toContain('a');
3026
+ expect(lines[0]).toContain('b');
3027
+ expect(lines.some((l) => l.includes('x') && l.includes('1'))).toBe(true);
3028
+ expect(lines.some((l) => l.includes('y') && l.includes('2'))).toBe(true);
3029
+ });
3030
+ it('truncates long cells and reports hidden rows / columns', () => {
3031
+ const rows = Array.from({ length: 15 }, (_, i) => ({
3032
+ a: i, b: 'x'.repeat(100), c: i, d: i, e: i, f: i, g: i, h: i,
3033
+ }));
3034
+ const out = renderVerifyPreview(rows, { maxRows: 5, maxCols: 3, cellMax: 10 });
3035
+ expect(out).toContain('and 10 more row');
3036
+ expect(out).toContain('more column');
3037
+ // cell gets truncated
3038
+ expect(out).toContain('xxxxxxxxxx');
3039
+ expect(out).not.toContain('xxxxxxxxxxx'); // never 11 consecutive
3040
+ });
3041
+ });