@jackwener/opencli 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/.github/workflows/build-extension.yml +2 -6
  2. package/.github/workflows/ci.yml +21 -1
  3. package/README.md +35 -6
  4. package/README.zh-CN.md +12 -5
  5. package/SKILL.md +2 -0
  6. package/dist/browser/cdp.d.ts +2 -1
  7. package/dist/browser/cdp.js +5 -0
  8. package/dist/browser/discover.d.ts +4 -1
  9. package/dist/browser/discover.js +6 -2
  10. package/dist/browser/errors.d.ts +2 -2
  11. package/dist/browser/errors.js +4 -12
  12. package/dist/browser/mcp.d.ts +2 -1
  13. package/dist/browser/page.d.ts +3 -0
  14. package/dist/browser/page.js +24 -1
  15. package/dist/build-manifest.d.ts +2 -0
  16. package/dist/build-manifest.js +39 -14
  17. package/dist/build-manifest.test.js +21 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +2 -1
  20. package/dist/cli-manifest.json +1567 -108
  21. package/dist/cli.js +68 -6
  22. package/dist/clis/36kr/article.d.ts +1 -0
  23. package/dist/clis/36kr/article.js +62 -0
  24. package/dist/clis/36kr/hot.d.ts +3 -0
  25. package/dist/clis/36kr/hot.js +80 -0
  26. package/dist/clis/36kr/hot.test.d.ts +1 -0
  27. package/dist/clis/36kr/hot.test.js +15 -0
  28. package/dist/clis/36kr/news.d.ts +1 -0
  29. package/dist/clis/36kr/news.js +51 -0
  30. package/dist/clis/36kr/news.test.d.ts +1 -0
  31. package/dist/clis/36kr/news.test.js +85 -0
  32. package/dist/clis/36kr/search.d.ts +1 -0
  33. package/dist/clis/36kr/search.js +72 -0
  34. package/dist/clis/bilibili/comments.d.ts +5 -0
  35. package/dist/clis/bilibili/comments.js +40 -0
  36. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  37. package/dist/clis/bilibili/comments.test.js +82 -0
  38. package/dist/clis/bluesky/feeds.yaml +29 -0
  39. package/dist/clis/bluesky/followers.yaml +33 -0
  40. package/dist/clis/bluesky/following.yaml +33 -0
  41. package/dist/clis/bluesky/profile.yaml +27 -0
  42. package/dist/clis/bluesky/search.yaml +34 -0
  43. package/dist/clis/bluesky/starter-packs.yaml +34 -0
  44. package/dist/clis/bluesky/thread.yaml +32 -0
  45. package/dist/clis/bluesky/trending.yaml +27 -0
  46. package/dist/clis/bluesky/user.yaml +34 -0
  47. package/dist/clis/chatgpt/ask.js +29 -14
  48. package/dist/clis/chatgpt/ax.d.ts +6 -0
  49. package/dist/clis/chatgpt/ax.js +172 -1
  50. package/dist/clis/chatgpt/model.d.ts +1 -0
  51. package/dist/clis/chatgpt/model.js +24 -0
  52. package/dist/clis/chatgpt/send.js +12 -3
  53. package/dist/clis/douban/download.d.ts +1 -0
  54. package/dist/clis/douban/download.js +67 -0
  55. package/dist/clis/douban/download.test.d.ts +1 -0
  56. package/dist/clis/douban/download.test.js +170 -0
  57. package/dist/clis/douban/photos.d.ts +1 -0
  58. package/dist/clis/douban/photos.js +34 -0
  59. package/dist/clis/douban/utils.d.ts +25 -0
  60. package/dist/clis/douban/utils.js +190 -1
  61. package/dist/clis/douban/utils.test.d.ts +1 -0
  62. package/dist/clis/douban/utils.test.js +64 -0
  63. package/dist/clis/imdb/person.d.ts +1 -0
  64. package/dist/clis/imdb/person.js +203 -0
  65. package/dist/clis/imdb/reviews.d.ts +1 -0
  66. package/dist/clis/imdb/reviews.js +88 -0
  67. package/dist/clis/imdb/search.d.ts +1 -0
  68. package/dist/clis/imdb/search.js +161 -0
  69. package/dist/clis/imdb/title.d.ts +1 -0
  70. package/dist/clis/imdb/title.js +93 -0
  71. package/dist/clis/imdb/top.d.ts +1 -0
  72. package/dist/clis/imdb/top.js +53 -0
  73. package/dist/clis/imdb/trending.d.ts +1 -0
  74. package/dist/clis/imdb/trending.js +52 -0
  75. package/dist/clis/imdb/utils.d.ts +46 -0
  76. package/dist/clis/imdb/utils.js +285 -0
  77. package/dist/clis/imdb/utils.test.d.ts +1 -0
  78. package/dist/clis/imdb/utils.test.js +88 -0
  79. package/dist/clis/jd/item.d.ts +4 -0
  80. package/dist/clis/jd/item.js +16 -15
  81. package/dist/clis/jd/item.test.js +16 -1
  82. package/dist/clis/linux-do/categories.yaml +38 -9
  83. package/dist/clis/linux-do/category.d.ts +1 -0
  84. package/dist/clis/linux-do/category.js +36 -0
  85. package/dist/clis/linux-do/feed.d.ts +45 -0
  86. package/dist/clis/linux-do/feed.js +397 -0
  87. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  88. package/dist/clis/linux-do/feed.test.js +118 -0
  89. package/dist/clis/linux-do/hot.d.ts +1 -0
  90. package/dist/clis/linux-do/hot.js +25 -0
  91. package/dist/clis/linux-do/latest.d.ts +1 -0
  92. package/dist/clis/linux-do/latest.js +18 -0
  93. package/dist/clis/linux-do/tags.yaml +41 -0
  94. package/dist/clis/linux-do/topic.yaml +41 -3
  95. package/dist/clis/linux-do/user-posts.yaml +67 -0
  96. package/dist/clis/linux-do/user-topics.yaml +54 -0
  97. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  98. package/dist/clis/paperreview/commands.test.js +243 -0
  99. package/dist/clis/paperreview/feedback.d.ts +1 -0
  100. package/dist/clis/paperreview/feedback.js +52 -0
  101. package/dist/clis/paperreview/review.d.ts +1 -0
  102. package/dist/clis/paperreview/review.js +37 -0
  103. package/dist/clis/paperreview/submit.d.ts +1 -0
  104. package/dist/clis/paperreview/submit.js +85 -0
  105. package/dist/clis/paperreview/utils.d.ts +46 -0
  106. package/dist/clis/paperreview/utils.js +197 -0
  107. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  108. package/dist/clis/paperreview/utils.test.js +49 -0
  109. package/dist/clis/producthunt/browse.d.ts +1 -0
  110. package/dist/clis/producthunt/browse.js +99 -0
  111. package/dist/clis/producthunt/hot.d.ts +1 -0
  112. package/dist/clis/producthunt/hot.js +110 -0
  113. package/dist/clis/producthunt/posts.d.ts +1 -0
  114. package/dist/clis/producthunt/posts.js +28 -0
  115. package/dist/clis/producthunt/today.d.ts +1 -0
  116. package/dist/clis/producthunt/today.js +35 -0
  117. package/dist/clis/producthunt/utils.d.ts +29 -0
  118. package/dist/clis/producthunt/utils.js +99 -0
  119. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  120. package/dist/clis/producthunt/utils.test.js +64 -0
  121. package/dist/clis/twitter/article.js +4 -28
  122. package/dist/clis/twitter/likes.d.ts +24 -0
  123. package/dist/clis/twitter/likes.js +217 -0
  124. package/dist/clis/twitter/likes.test.d.ts +1 -0
  125. package/dist/clis/twitter/likes.test.js +85 -0
  126. package/dist/clis/twitter/profile.js +4 -28
  127. package/dist/clis/twitter/search.js +2 -1
  128. package/dist/clis/twitter/search.test.js +2 -0
  129. package/dist/clis/twitter/shared.d.ts +6 -0
  130. package/dist/clis/twitter/shared.js +35 -0
  131. package/dist/clis/twitter/timeline.js +2 -13
  132. package/dist/clis/twitter/trending.js +29 -61
  133. package/dist/clis/v2ex/hot.yaml +17 -3
  134. package/dist/clis/weixin/download.d.ts +17 -0
  135. package/dist/clis/weixin/download.js +88 -20
  136. package/dist/clis/weread/book.js +2 -2
  137. package/dist/clis/weread/commands.test.d.ts +3 -0
  138. package/dist/clis/weread/commands.test.js +43 -0
  139. package/dist/clis/weread/highlights.js +2 -2
  140. package/dist/clis/weread/notebooks.js +2 -2
  141. package/dist/clis/weread/notes.js +3 -3
  142. package/dist/clis/weread/shelf.js +2 -2
  143. package/dist/clis/weread/utils.d.ts +4 -4
  144. package/dist/clis/weread/utils.js +32 -14
  145. package/dist/clis/weread/utils.test.js +1 -28
  146. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  147. package/dist/clis/xiaohongshu/comments.js +74 -0
  148. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  149. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  150. package/dist/clis/xiaohongshu/publish.js +179 -47
  151. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  152. package/dist/clis/xiaohongshu/publish.test.js +131 -0
  153. package/dist/clis/xiaohongshu/search.d.ts +8 -1
  154. package/dist/clis/xiaohongshu/search.js +20 -1
  155. package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
  156. package/dist/clis/xiaohongshu/search.test.js +32 -1
  157. package/dist/commanderAdapter.d.ts +1 -0
  158. package/dist/commanderAdapter.js +176 -29
  159. package/dist/commanderAdapter.test.d.ts +1 -0
  160. package/dist/commanderAdapter.test.js +62 -0
  161. package/dist/daemon.js +17 -1
  162. package/dist/discovery.js +48 -42
  163. package/dist/doctor.d.ts +2 -2
  164. package/dist/doctor.js +11 -4
  165. package/dist/download/index.js +63 -51
  166. package/dist/download/index.test.js +17 -4
  167. package/dist/engine.test.js +42 -0
  168. package/dist/errors.d.ts +4 -2
  169. package/dist/errors.js +17 -34
  170. package/dist/execution.d.ts +1 -3
  171. package/dist/execution.js +66 -8
  172. package/dist/execution.test.d.ts +1 -0
  173. package/dist/execution.test.js +40 -0
  174. package/dist/external.js +6 -1
  175. package/dist/hooks.js +2 -0
  176. package/dist/main.js +6 -0
  177. package/dist/output.js +5 -1
  178. package/dist/pipeline/executor.js +3 -4
  179. package/dist/plugin-manifest.d.ts +70 -0
  180. package/dist/plugin-manifest.js +160 -0
  181. package/dist/plugin-manifest.test.d.ts +4 -0
  182. package/dist/plugin-manifest.test.js +179 -0
  183. package/dist/plugin-scaffold.d.ts +28 -0
  184. package/dist/plugin-scaffold.js +142 -0
  185. package/dist/plugin-scaffold.test.d.ts +4 -0
  186. package/dist/plugin-scaffold.test.js +83 -0
  187. package/dist/plugin.d.ts +82 -11
  188. package/dist/plugin.js +870 -84
  189. package/dist/plugin.test.js +1032 -17
  190. package/dist/registry.d.ts +4 -0
  191. package/dist/registry.js +2 -0
  192. package/dist/runtime-detect.d.ts +21 -0
  193. package/dist/runtime-detect.js +32 -0
  194. package/dist/runtime-detect.test.d.ts +1 -0
  195. package/dist/runtime-detect.test.js +27 -0
  196. package/dist/runtime.d.ts +1 -0
  197. package/dist/runtime.js +2 -2
  198. package/dist/serialization.d.ts +2 -0
  199. package/dist/serialization.js +6 -0
  200. package/dist/types.d.ts +3 -0
  201. package/dist/update-check.d.ts +22 -0
  202. package/dist/update-check.js +112 -0
  203. package/dist/weixin-download.test.d.ts +1 -0
  204. package/dist/weixin-download.test.js +30 -0
  205. package/dist/weread-private-api-regression.test.d.ts +1 -0
  206. package/dist/weread-private-api-regression.test.js +122 -0
  207. package/dist/yaml-schema.d.ts +3 -0
  208. package/dist/yaml-schema.js +18 -1
  209. package/docs/.vitepress/config.mts +4 -0
  210. package/docs/adapters/browser/36kr.md +47 -0
  211. package/docs/adapters/browser/bluesky.md +53 -0
  212. package/docs/adapters/browser/douban.md +14 -0
  213. package/docs/adapters/browser/imdb.md +47 -0
  214. package/docs/adapters/browser/jd.md +2 -2
  215. package/docs/adapters/browser/linux-do.md +181 -20
  216. package/docs/adapters/browser/paperreview.md +43 -0
  217. package/docs/adapters/browser/producthunt.md +49 -0
  218. package/docs/adapters/desktop/chatgpt.md +5 -0
  219. package/docs/adapters/index.md +6 -2
  220. package/docs/advanced/download.md +4 -0
  221. package/docs/advanced/rate-limiter-plugin.md +99 -0
  222. package/docs/guide/electron-app-cli.md +200 -0
  223. package/docs/guide/getting-started.md +1 -0
  224. package/docs/guide/plugins.md +97 -0
  225. package/docs/zh/guide/electron-app-cli.md +188 -0
  226. package/docs/zh/guide/getting-started.md +1 -0
  227. package/docs/zh/guide/plugins.md +65 -0
  228. package/extension/package.json +1 -0
  229. package/extension/scripts/package-release.mjs +179 -0
  230. package/extension/src/background.ts +2 -0
  231. package/package.json +4 -1
  232. package/scripts/postinstall.js +10 -0
  233. package/src/browser/cdp.ts +8 -1
  234. package/src/browser/discover.ts +8 -3
  235. package/src/browser/errors.ts +13 -14
  236. package/src/browser/mcp.ts +2 -1
  237. package/src/browser/page.ts +24 -1
  238. package/src/build-manifest.test.ts +23 -0
  239. package/src/build-manifest.ts +40 -15
  240. package/src/capabilityRouting.ts +2 -1
  241. package/src/cli.ts +69 -6
  242. package/src/clis/36kr/article.ts +69 -0
  243. package/src/clis/36kr/hot.test.ts +19 -0
  244. package/src/clis/36kr/hot.ts +100 -0
  245. package/src/clis/36kr/news.test.ts +90 -0
  246. package/src/clis/36kr/news.ts +54 -0
  247. package/src/clis/36kr/search.ts +78 -0
  248. package/src/clis/bilibili/comments.test.ts +102 -0
  249. package/src/clis/bilibili/comments.ts +44 -0
  250. package/src/clis/bluesky/feeds.yaml +29 -0
  251. package/src/clis/bluesky/followers.yaml +33 -0
  252. package/src/clis/bluesky/following.yaml +33 -0
  253. package/src/clis/bluesky/profile.yaml +27 -0
  254. package/src/clis/bluesky/search.yaml +34 -0
  255. package/src/clis/bluesky/starter-packs.yaml +34 -0
  256. package/src/clis/bluesky/thread.yaml +32 -0
  257. package/src/clis/bluesky/trending.yaml +27 -0
  258. package/src/clis/bluesky/user.yaml +34 -0
  259. package/src/clis/chatgpt/ask.ts +28 -14
  260. package/src/clis/chatgpt/ax.ts +180 -1
  261. package/src/clis/chatgpt/model.ts +27 -0
  262. package/src/clis/chatgpt/send.ts +16 -6
  263. package/src/clis/douban/download.test.ts +196 -0
  264. package/src/clis/douban/download.ts +78 -0
  265. package/src/clis/douban/photos.ts +36 -0
  266. package/src/clis/douban/utils.test.ts +97 -0
  267. package/src/clis/douban/utils.ts +232 -1
  268. package/src/clis/imdb/person.ts +232 -0
  269. package/src/clis/imdb/reviews.ts +111 -0
  270. package/src/clis/imdb/search.ts +179 -0
  271. package/src/clis/imdb/title.ts +121 -0
  272. package/src/clis/imdb/top.ts +67 -0
  273. package/src/clis/imdb/trending.ts +66 -0
  274. package/src/clis/imdb/utils.test.ts +117 -0
  275. package/src/clis/imdb/utils.ts +305 -0
  276. package/src/clis/jd/item.test.ts +18 -1
  277. package/src/clis/jd/item.ts +18 -15
  278. package/src/clis/linux-do/categories.yaml +38 -9
  279. package/src/clis/linux-do/category.ts +37 -0
  280. package/src/clis/linux-do/feed.test.ts +132 -0
  281. package/src/clis/linux-do/feed.ts +501 -0
  282. package/src/clis/linux-do/hot.ts +26 -0
  283. package/src/clis/linux-do/latest.ts +19 -0
  284. package/src/clis/linux-do/tags.yaml +41 -0
  285. package/src/clis/linux-do/topic.yaml +41 -3
  286. package/src/clis/linux-do/user-posts.yaml +67 -0
  287. package/src/clis/linux-do/user-topics.yaml +54 -0
  288. package/src/clis/paperreview/commands.test.ts +283 -0
  289. package/src/clis/paperreview/feedback.ts +64 -0
  290. package/src/clis/paperreview/review.ts +47 -0
  291. package/src/clis/paperreview/submit.ts +119 -0
  292. package/src/clis/paperreview/utils.test.ts +68 -0
  293. package/src/clis/paperreview/utils.ts +276 -0
  294. package/src/clis/producthunt/browse.ts +109 -0
  295. package/src/clis/producthunt/hot.ts +127 -0
  296. package/src/clis/producthunt/posts.ts +29 -0
  297. package/src/clis/producthunt/today.ts +37 -0
  298. package/src/clis/producthunt/utils.test.ts +72 -0
  299. package/src/clis/producthunt/utils.ts +122 -0
  300. package/src/clis/twitter/article.ts +5 -28
  301. package/src/clis/twitter/likes.test.ts +91 -0
  302. package/src/clis/twitter/likes.ts +256 -0
  303. package/src/clis/twitter/profile.ts +5 -28
  304. package/src/clis/twitter/search.test.ts +2 -0
  305. package/src/clis/twitter/search.ts +3 -1
  306. package/src/clis/twitter/shared.ts +45 -0
  307. package/src/clis/twitter/timeline.ts +2 -13
  308. package/src/clis/twitter/trending.ts +29 -77
  309. package/src/clis/v2ex/hot.yaml +17 -3
  310. package/src/clis/weixin/download.ts +114 -20
  311. package/src/clis/weread/book.ts +2 -2
  312. package/src/clis/weread/commands.test.ts +57 -0
  313. package/src/clis/weread/highlights.ts +2 -2
  314. package/src/clis/weread/notebooks.ts +2 -2
  315. package/src/clis/weread/notes.ts +3 -3
  316. package/src/clis/weread/shelf.ts +2 -2
  317. package/src/clis/weread/utils.test.ts +1 -32
  318. package/src/clis/weread/utils.ts +41 -16
  319. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  320. package/src/clis/xiaohongshu/comments.ts +81 -0
  321. package/src/clis/xiaohongshu/publish.test.ts +151 -0
  322. package/src/clis/xiaohongshu/publish.ts +206 -54
  323. package/src/clis/xiaohongshu/search.test.ts +39 -1
  324. package/src/clis/xiaohongshu/search.ts +19 -1
  325. package/src/commanderAdapter.test.ts +78 -0
  326. package/src/commanderAdapter.ts +188 -24
  327. package/src/daemon.ts +19 -1
  328. package/src/discovery.ts +49 -48
  329. package/src/doctor.ts +15 -5
  330. package/src/download/index.test.ts +14 -4
  331. package/src/download/index.ts +67 -55
  332. package/src/engine.test.ts +38 -0
  333. package/src/errors.ts +26 -63
  334. package/src/execution.test.ts +47 -0
  335. package/src/execution.ts +67 -9
  336. package/src/external.ts +6 -1
  337. package/src/hooks.ts +1 -0
  338. package/src/main.ts +7 -0
  339. package/src/output.ts +3 -1
  340. package/src/pipeline/executor.ts +4 -6
  341. package/src/plugin-manifest.test.ts +223 -0
  342. package/src/plugin-manifest.ts +206 -0
  343. package/src/plugin-scaffold.test.ts +98 -0
  344. package/src/plugin-scaffold.ts +170 -0
  345. package/src/plugin.test.ts +1104 -17
  346. package/src/plugin.ts +1101 -86
  347. package/src/registry.ts +6 -1
  348. package/src/runtime-detect.test.ts +30 -0
  349. package/src/runtime-detect.ts +36 -0
  350. package/src/runtime.ts +3 -3
  351. package/src/serialization.ts +4 -0
  352. package/src/types.ts +3 -0
  353. package/src/update-check.ts +114 -0
  354. package/src/weixin-download.test.ts +64 -0
  355. package/src/weread-private-api-regression.test.ts +150 -0
  356. package/src/yaml-schema.ts +20 -0
  357. package/tests/e2e/browser-auth.test.ts +13 -9
  358. package/tests/e2e/browser-public-extended.test.ts +1 -1
  359. package/tests/e2e/browser-public.test.ts +62 -4
  360. package/tests/e2e/helpers.ts +2 -1
  361. package/tests/e2e/public-commands.test.ts +37 -3
  362. package/tests/smoke/api-health.test.ts +1 -1
  363. package/vitest.config.ts +10 -0
  364. package/dist/clis/linux-do/category.yaml +0 -51
  365. package/dist/clis/linux-do/hot.yaml +0 -50
  366. package/dist/clis/linux-do/latest.yaml +0 -40
  367. package/src/clis/linux-do/category.yaml +0 -51
  368. package/src/clis/linux-do/hot.yaml +0 -50
  369. package/src/clis/linux-do/latest.yaml +0 -40
@@ -16,7 +16,32 @@ import { type CliCommand, fullName, getRegistry } from './registry.js';
16
16
  import { formatRegistryHelpText } from './serialization.js';
17
17
  import { render as renderOutput } from './output.js';
18
18
  import { executeCommand } from './execution.js';
19
- import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
19
+ import {
20
+ CliError,
21
+ ERROR_ICONS,
22
+ getErrorMessage,
23
+ BrowserConnectError,
24
+ AuthRequiredError,
25
+ TimeoutError,
26
+ SelectorError,
27
+ EmptyResultError,
28
+ ArgumentError,
29
+ AdapterLoadError,
30
+ CommandExecutionError,
31
+ } from './errors.js';
32
+ import { checkDaemonStatus } from './browser/discover.js';
33
+
34
+ export function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown {
35
+ if (argType !== 'bool') return value;
36
+ if (typeof value === 'boolean') return value;
37
+ if (value == null || value === '') return false;
38
+
39
+ const normalized = String(value).trim().toLowerCase();
40
+ if (normalized === 'true') return true;
41
+ if (normalized === 'false') return false;
42
+
43
+ throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
44
+ }
20
45
 
21
46
  /**
22
47
  * Register a single CliCommand as a Commander subcommand.
@@ -24,7 +49,8 @@ import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
24
49
  export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): void {
25
50
  if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) return;
26
51
 
27
- const subCmd = siteCmd.command(cmd.name).description(cmd.description);
52
+ const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
53
+ const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
28
54
 
29
55
  // Register positional args first, then named options
30
56
  const positionalArgs: typeof cmd.args = [];
@@ -51,24 +77,29 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
51
77
  const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts as Record<string, unknown> : {};
52
78
  const startTime = Date.now();
53
79
 
54
- // ── Collect kwargs ──────────────────────────────────────────────────
55
- const kwargs: Record<string, unknown> = {};
56
- for (let i = 0; i < positionalArgs.length; i++) {
57
- const v = actionArgs[i];
58
- if (v !== undefined) kwargs[positionalArgs[i].name] = v;
59
- }
60
- for (const arg of cmd.args) {
61
- if (arg.positional) continue;
62
- const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
63
- const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
64
- if (v !== undefined) kwargs[arg.name] = v;
65
- }
66
-
67
80
  // ── Execute + render ────────────────────────────────────────────────
68
81
  try {
82
+ // ── Collect kwargs ────────────────────────────────────────────────
83
+ const kwargs: Record<string, unknown> = {};
84
+ for (let i = 0; i < positionalArgs.length; i++) {
85
+ const v = actionArgs[i];
86
+ if (v !== undefined) kwargs[positionalArgs[i].name] = v;
87
+ }
88
+ for (const arg of cmd.args) {
89
+ if (arg.positional) continue;
90
+ const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
91
+ const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
92
+ if (v !== undefined) kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
93
+ }
94
+
69
95
  const verbose = optionsRecord.verbose === true;
70
96
  const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
71
97
  if (verbose) process.env.OPENCLI_VERBOSE = '1';
98
+ if (cmd.deprecated) {
99
+ const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
100
+ const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
101
+ console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
102
+ }
72
103
 
73
104
  const result = await executeCommand(cmd, kwargs, verbose);
74
105
 
@@ -85,20 +116,153 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
85
116
  footerExtra: resolved.footerExtra?.(kwargs),
86
117
  });
87
118
  } catch (err) {
88
- if (err instanceof CliError) {
89
- const icon = ERROR_ICONS[err.code] ?? '⚠️';
90
- console.error(chalk.red(`${icon} ${err.message}`));
91
- if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
92
- } else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
93
- console.error(chalk.red(err.stack));
94
- } else {
95
- console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
96
- }
119
+ await renderError(err, fullName(cmd), optionsRecord.verbose === true);
97
120
  process.exitCode = 1;
98
121
  }
99
122
  });
100
123
  }
101
124
 
125
+ // ── Error rendering ──────────────────────────────────────────────────────────
126
+
127
+ const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
128
+
129
+ /** Pattern-based classifier for untyped errors thrown by adapters. */
130
+ function classifyGenericError(msg: string): 'auth' | 'http' | 'not-found' | 'other' {
131
+ const m = msg.toLowerCase();
132
+ if (/not logged in|login required|please log in|未登录|请先登录|authentication required|cookie expired/.test(m)) return 'auth';
133
+ // Match "HTTP 404", "status: 500", "status 403", bare "404 Not Found", etc.
134
+ if (/\b(status[: ]+)?[45]\d{2}\b|http[/ ][45]\d{2}/.test(m)) return 'http';
135
+ if (/not found|未找到|could not find|no .+ found/.test(m)) return 'not-found';
136
+ return 'other';
137
+ }
138
+
139
+ /** Render a status line for BrowserConnectError based on real-time or kind-derived state. */
140
+ function renderBridgeStatus(running: boolean, extensionConnected: boolean): void {
141
+ const ok = chalk.green('✓');
142
+ const fail = chalk.red('✗');
143
+ console.error(` Daemon ${running ? ok : fail} ${running ? 'running' : 'not running'}`);
144
+ console.error(` Extension ${extensionConnected ? ok : fail} ${extensionConnected ? 'connected' : 'not connected'}`);
145
+ console.error();
146
+ if (!running) {
147
+ console.error(chalk.yellow(' Run the command again — daemon should auto-start.'));
148
+ console.error(chalk.dim(' Still failing? Run: opencli doctor'));
149
+ } else if (!extensionConnected) {
150
+ console.error(chalk.yellow(' Install the Browser Bridge extension to continue:'));
151
+ console.error(chalk.dim(' 1. Download from github.com/jackwener/opencli/releases'));
152
+ console.error(chalk.dim(' 2. chrome://extensions → Enable Developer Mode → Load unpacked'));
153
+ } else {
154
+ console.error(chalk.yellow(' Connection failed despite extension being active.'));
155
+ console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
156
+ }
157
+ }
158
+
159
+ async function renderError(err: unknown, cmdName: string, verbose: boolean): Promise<void> {
160
+ // ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
161
+ if (err instanceof BrowserConnectError) {
162
+ console.error(chalk.red('🔌 Browser Bridge not connected'));
163
+ console.error();
164
+ try {
165
+ // 300ms matches execution.ts — localhost responds in <50ms when running.
166
+ const status = await checkDaemonStatus({ timeout: 300 });
167
+ renderBridgeStatus(status.running, status.extensionConnected);
168
+ } catch (_statusErr) {
169
+ // checkDaemonStatus itself failed — derive best-guess state from kind.
170
+ const running = err.kind !== 'daemon-not-running';
171
+ const extensionConnected = err.kind === 'command-failed';
172
+ renderBridgeStatus(running, extensionConnected);
173
+ }
174
+ return;
175
+ }
176
+
177
+ // ── AuthRequiredError ─────────────────────────────────────────────────
178
+ if (err instanceof AuthRequiredError) {
179
+ console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
180
+ // Respect custom hints set by the adapter; fall back to generic guidance.
181
+ console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
182
+ return;
183
+ }
184
+
185
+ // ── TimeoutError ──────────────────────────────────────────────────────
186
+ if (err instanceof TimeoutError) {
187
+ console.error(chalk.red(`⏱ ${err.message}`));
188
+ console.error(chalk.yellow('→ Try again, or raise the limit:'));
189
+ console.error(chalk.dim(` OPENCLI_BROWSER_COMMAND_TIMEOUT=60 ${cmdName}`));
190
+ return;
191
+ }
192
+
193
+ // ── SelectorError / EmptyResultError: likely outdated adapter ─────────
194
+ if (err instanceof SelectorError || err instanceof EmptyResultError) {
195
+ const icon = ERROR_ICONS[err.code] ?? '⚠️';
196
+ console.error(chalk.red(`${icon} ${err.message}`));
197
+ console.error(chalk.yellow('→ The page structure may have changed — this adapter may be outdated.'));
198
+ console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
199
+ console.error(chalk.dim(` Report: ${ISSUES_URL}`));
200
+ return;
201
+ }
202
+
203
+ // ── ArgumentError ─────────────────────────────────────────────────────
204
+ if (err instanceof ArgumentError) {
205
+ console.error(chalk.red(`❌ ${err.message}`));
206
+ if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
207
+ return;
208
+ }
209
+
210
+ // ── AdapterLoadError ──────────────────────────────────────────────────
211
+ if (err instanceof AdapterLoadError) {
212
+ console.error(chalk.red(`📦 ${err.message}`));
213
+ if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
214
+ return;
215
+ }
216
+
217
+ // ── CommandExecutionError ─────────────────────────────────────────────
218
+ if (err instanceof CommandExecutionError) {
219
+ console.error(chalk.red(`💥 ${err.message}`));
220
+ if (err.hint) {
221
+ console.error(chalk.yellow(`→ ${err.hint}`));
222
+ } else {
223
+ console.error(chalk.dim(` Add --verbose for details, or report: ${ISSUES_URL}`));
224
+ }
225
+ return;
226
+ }
227
+
228
+ // ── Other typed CliError (fallback for future codes) ──────────────────
229
+ if (err instanceof CliError) {
230
+ const icon = ERROR_ICONS[err.code] ?? '⚠️';
231
+ console.error(chalk.red(`${icon} ${err.message}`));
232
+ if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
233
+ return;
234
+ }
235
+
236
+ // ── Generic Error from adapters: classify by message pattern ──────────
237
+ const msg = getErrorMessage(err);
238
+ const kind = classifyGenericError(msg);
239
+
240
+ if (kind === 'auth') {
241
+ console.error(chalk.red(`🔒 ${msg}`));
242
+ console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
243
+ return;
244
+ }
245
+ if (kind === 'http') {
246
+ console.error(chalk.red(`🌐 ${msg}`));
247
+ console.error(chalk.yellow('→ Check your login status, or the site may be temporarily unavailable.'));
248
+ return;
249
+ }
250
+ if (kind === 'not-found') {
251
+ console.error(chalk.red(`📭 ${msg}`));
252
+ console.error(chalk.yellow('→ The resource was not found. The adapter or page structure may have changed.'));
253
+ console.error(chalk.dim(` Report: ${ISSUES_URL}`));
254
+ return;
255
+ }
256
+
257
+ // ── Unknown error: show stack in verbose mode ─────────────────────────
258
+ if (verbose && err instanceof Error && err.stack) {
259
+ console.error(chalk.red(err.stack));
260
+ } else {
261
+ console.error(chalk.red(`💥 Unexpected error: ${msg}`));
262
+ console.error(chalk.dim(` Run with --verbose for details, or report: ${ISSUES_URL}`));
263
+ }
264
+ }
265
+
102
266
  /**
103
267
  * Register all commands from the registry onto a Commander program.
104
268
  */
package/src/daemon.ts CHANGED
@@ -29,6 +29,7 @@ const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
29
29
  // ─── State ───────────────────────────────────────────────────────────
30
30
 
31
31
  let extensionWs: WebSocket | null = null;
32
+ let extensionVersion: string | null = null;
32
33
  const pending = new Map<string, {
33
34
  resolve: (data: unknown) => void;
34
35
  reject: (error: Error) => void;
@@ -117,6 +118,7 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
117
118
  jsonResponse(res, 200, {
118
119
  ok: true,
119
120
  extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
121
+ extensionVersion,
120
122
  pending: pending.size,
121
123
  });
122
124
  return;
@@ -222,6 +224,12 @@ wss.on('connection', (ws: WebSocket) => {
222
224
  try {
223
225
  const msg = JSON.parse(data.toString());
224
226
 
227
+ // Handle hello message from extension (version handshake)
228
+ if (msg.type === 'hello') {
229
+ extensionVersion = typeof msg.version === 'string' ? msg.version : null;
230
+ return;
231
+ }
232
+
225
233
  // Handle log messages from extension
226
234
  if (msg.type === 'log') {
227
235
  const prefix = msg.level === 'error' ? '❌' : msg.level === 'warn' ? '⚠️' : '📋';
@@ -247,6 +255,7 @@ wss.on('connection', (ws: WebSocket) => {
247
255
  clearInterval(heartbeatInterval);
248
256
  if (extensionWs === ws) {
249
257
  extensionWs = null;
258
+ extensionVersion = null;
250
259
  // Reject all pending requests since the extension is gone
251
260
  for (const [id, p] of pending) {
252
261
  clearTimeout(p.timer);
@@ -258,7 +267,16 @@ wss.on('connection', (ws: WebSocket) => {
258
267
 
259
268
  ws.on('error', () => {
260
269
  clearInterval(heartbeatInterval);
261
- if (extensionWs === ws) extensionWs = null;
270
+ if (extensionWs === ws) {
271
+ extensionWs = null;
272
+ extensionVersion = null;
273
+ // Reject pending requests in case 'close' does not follow this 'error'
274
+ for (const [, p] of pending) {
275
+ clearTimeout(p.timer);
276
+ p.reject(new Error('Extension disconnected'));
277
+ }
278
+ pending.clear();
279
+ }
262
280
  });
263
281
  });
264
282
 
package/src/discovery.ts CHANGED
@@ -23,7 +23,7 @@ export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
23
23
  /** Matches files that register commands via cli() or lifecycle hooks */
24
24
  const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
25
25
 
26
- import type { YamlCliDefinition } from './yaml-schema.js';
26
+ import { type YamlCliDefinition, parseYamlArgs } from './yaml-schema.js';
27
27
 
28
28
  function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
29
29
  if (!rawStrategy) return fallback;
@@ -77,6 +77,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
77
77
  pipeline: entry.pipeline,
78
78
  timeoutSeconds: entry.timeout,
79
79
  source: `manifest:${entry.site}/${entry.name}`,
80
+ deprecated: entry.deprecated,
81
+ replacedBy: entry.replacedBy,
80
82
  navigateBefore: entry.navigateBefore,
81
83
  };
82
84
  registerCommand(cmd);
@@ -96,6 +98,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
96
98
  columns: entry.columns,
97
99
  timeoutSeconds: entry.timeout,
98
100
  source: modulePath,
101
+ deprecated: entry.deprecated,
102
+ replacedBy: entry.replacedBy,
99
103
  navigateBefore: entry.navigateBefore,
100
104
  _lazy: true,
101
105
  _modulePath: modulePath,
@@ -123,24 +127,20 @@ async function discoverClisFromFs(dir: string): Promise<void> {
123
127
  const site = entry.name;
124
128
  const siteDir = path.join(dir, site);
125
129
  const files = await fs.promises.readdir(siteDir);
126
- const filePromises: Promise<unknown>[] = [];
127
- for (const file of files) {
130
+ await Promise.all(files.map(async (file) => {
128
131
  const filePath = path.join(siteDir, file);
129
132
  if (file.endsWith('.yaml') || file.endsWith('.yml')) {
130
- filePromises.push(registerYamlCli(filePath, site));
133
+ await registerYamlCli(filePath, site);
131
134
  } else if (
132
135
  (file.endsWith('.js') && !file.endsWith('.d.js')) ||
133
136
  (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))
134
137
  ) {
135
- if (!(await isCliModule(filePath))) continue;
136
- filePromises.push(
137
- import(pathToFileURL(filePath).href).catch((err) => {
138
- log.warn(`Failed to load module ${filePath}: ${getErrorMessage(err)}`);
139
- })
140
- );
138
+ if (!(await isCliModule(filePath))) return;
139
+ await import(pathToFileURL(filePath).href).catch((err) => {
140
+ log.warn(`Failed to load module ${filePath}: ${getErrorMessage(err)}`);
141
+ });
141
142
  }
142
- }
143
- await Promise.all(filePromises);
143
+ }));
144
144
  });
145
145
  await Promise.all(sitePromises);
146
146
  }
@@ -158,20 +158,7 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
158
158
  const strategy = parseStrategy(strategyStr);
159
159
  const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
160
160
 
161
- const args: Arg[] = [];
162
- if (cliDef.args && typeof cliDef.args === 'object') {
163
- for (const [argName, argDef] of Object.entries(cliDef.args)) {
164
- args.push({
165
- name: argName,
166
- type: argDef?.type ?? 'str',
167
- default: argDef?.default,
168
- required: argDef?.required ?? false,
169
- positional: argDef?.positional ?? false,
170
- help: argDef?.description ?? argDef?.help ?? '',
171
- choices: argDef?.choices,
172
- });
173
- }
174
- }
161
+ const args = parseYamlArgs(cliDef.args);
175
162
 
176
163
  const cmd: CliCommand = {
177
164
  site,
@@ -185,6 +172,8 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
185
172
  pipeline: cliDef.pipeline,
186
173
  timeoutSeconds: cliDef.timeout,
187
174
  source: filePath,
175
+ deprecated: (cliDef as Record<string, unknown>).deprecated as boolean | string | undefined,
176
+ replacedBy: (cliDef as Record<string, unknown>).replacedBy as string | undefined,
188
177
  navigateBefore: cliDef.navigateBefore,
189
178
  };
190
179
 
@@ -202,10 +191,11 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
202
191
  export async function discoverPlugins(): Promise<void> {
203
192
  try { await fs.promises.access(PLUGINS_DIR); } catch { return; }
204
193
  const entries = await fs.promises.readdir(PLUGINS_DIR, { withFileTypes: true });
205
- for (const entry of entries) {
206
- if (!entry.isDirectory()) continue;
207
- await discoverPluginDir(path.join(PLUGINS_DIR, entry.name), entry.name);
208
- }
194
+ await Promise.all(entries.map(async (entry) => {
195
+ const pluginDir = path.join(PLUGINS_DIR, entry.name);
196
+ if (!(await isDiscoverablePluginDir(entry, pluginDir))) return;
197
+ await discoverPluginDir(pluginDir, entry.name);
198
+ }));
209
199
  }
210
200
 
211
201
  /**
@@ -215,33 +205,29 @@ export async function discoverPlugins(): Promise<void> {
215
205
  async function discoverPluginDir(dir: string, site: string): Promise<void> {
216
206
  const files = await fs.promises.readdir(dir);
217
207
  const fileSet = new Set(files);
218
- const promises: Promise<unknown>[] = [];
219
- for (const file of files) {
208
+ await Promise.all(files.map(async (file) => {
220
209
  const filePath = path.join(dir, file);
221
210
  if (file.endsWith('.yaml') || file.endsWith('.yml')) {
222
- promises.push(registerYamlCli(filePath, site));
211
+ await registerYamlCli(filePath, site);
223
212
  } else if (file.endsWith('.js') && !file.endsWith('.d.js')) {
224
- if (!(await isCliModule(filePath))) continue;
225
- promises.push(
226
- import(pathToFileURL(filePath).href).catch((err) => {
227
- log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
228
- })
229
- );
213
+ if (!(await isCliModule(filePath))) return;
214
+ await import(pathToFileURL(filePath).href).catch((err) => {
215
+ log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
216
+ });
230
217
  } else if (
231
218
  file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts')
232
219
  ) {
233
- // Skip .ts if a compiled .js sibling exists (production mode can't load .ts)
234
220
  const jsFile = file.replace(/\.ts$/, '.js');
235
- if (fileSet.has(jsFile)) continue;
236
- if (!(await isCliModule(filePath))) continue;
237
- promises.push(
238
- import(pathToFileURL(filePath).href).catch((err) => {
239
- log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
240
- })
221
+ // Prefer compiled .js — skip the .ts source file
222
+ if (fileSet.has(jsFile)) return;
223
+ // No compiled .js found — cannot import raw .ts in production Node.js.
224
+ // This typically means esbuild transpilation failed during plugin install.
225
+ log.warn(
226
+ `Plugin ${site}/${file}: no compiled .js found. ` +
227
+ `Run "opencli plugin update ${site}" to re-transpile, or install esbuild.`
241
228
  );
242
229
  }
243
- }
244
- await Promise.all(promises);
230
+ }));
245
231
  }
246
232
 
247
233
  async function isCliModule(filePath: string): Promise<boolean> {
@@ -253,3 +239,18 @@ async function isCliModule(filePath: string): Promise<boolean> {
253
239
  return false;
254
240
  }
255
241
  }
242
+
243
+ async function isDiscoverablePluginDir(entry: fs.Dirent, pluginDir: string): Promise<boolean> {
244
+ if (entry.isDirectory()) return true;
245
+ if (!entry.isSymbolicLink()) return false;
246
+
247
+ try {
248
+ return (await fs.promises.stat(pluginDir)).isDirectory();
249
+ } catch (err) {
250
+ const code = (err as NodeJS.ErrnoException).code;
251
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
252
+ log.warn(`Failed to inspect plugin link ${pluginDir}: ${getErrorMessage(err)}`);
253
+ }
254
+ return false;
255
+ }
256
+ }
package/src/doctor.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * opencli doctor — diagnose and fix browser connectivity.
2
+ * opencli doctor — diagnose browser connectivity.
3
3
  *
4
4
  * Simplified for the daemon-based architecture. No more token management,
5
5
  * MCP path discovery, or config file scanning.
@@ -11,9 +11,9 @@ import { checkDaemonStatus } from './browser/discover.js';
11
11
  import { BrowserBridge } from './browser/index.js';
12
12
  import { listSessions } from './browser/daemon-client.js';
13
13
  import { getErrorMessage } from './errors.js';
14
+ import { getRuntimeLabel } from './runtime-detect.js';
14
15
 
15
16
  export type DoctorOptions = {
16
- fix?: boolean;
17
17
  yes?: boolean;
18
18
  live?: boolean;
19
19
  sessions?: boolean;
@@ -30,6 +30,7 @@ export type DoctorReport = {
30
30
  cliVersion?: string;
31
31
  daemonRunning: boolean;
32
32
  extensionConnected: boolean;
33
+ extensionVersion?: string;
33
34
  connectivity?: ConnectivityResult;
34
35
  sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
35
36
  issues: string[];
@@ -85,7 +86,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
85
86
  issues.push(
86
87
  'Daemon is running but the Chrome extension is not connected.\n' +
87
88
  'Please install the opencli Browser Bridge extension:\n' +
88
- ' 1. Download from GitHub Releases\n' +
89
+ ' 1. Download from https://github.com/jackwener/opencli/releases\n' +
89
90
  ' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
90
91
  ' 3. Click "Load unpacked" → select the extension folder',
91
92
  );
@@ -94,10 +95,18 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
94
95
  issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
95
96
  }
96
97
 
98
+ if (status.extensionVersion && opts.cliVersion && status.extensionVersion !== opts.cliVersion) {
99
+ issues.push(
100
+ `Extension version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
101
+ ' Download the latest extension from: https://github.com/jackwener/opencli/releases',
102
+ );
103
+ }
104
+
97
105
  return {
98
106
  cliVersion: opts.cliVersion,
99
107
  daemonRunning: status.running,
100
108
  extensionConnected: status.extensionConnected,
109
+ extensionVersion: status.extensionVersion,
101
110
  connectivity,
102
111
  sessions,
103
112
  issues,
@@ -105,7 +114,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
105
114
  }
106
115
 
107
116
  export function renderBrowserDoctorReport(report: DoctorReport): string {
108
- const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
117
+ const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
109
118
 
110
119
  // Daemon status
111
120
  const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
@@ -113,7 +122,8 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
113
122
 
114
123
  // Extension status
115
124
  const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
116
- lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}`);
125
+ const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
126
+ lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
117
127
 
118
128
  // Connectivity
119
129
  if (report.connectivity) {
@@ -6,12 +6,17 @@ import { afterEach, describe, expect, it } from 'vitest';
6
6
  import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
7
7
 
8
8
  const servers: http.Server[] = [];
9
+ const tempDirs: string[] = [];
9
10
 
10
11
  afterEach(async () => {
11
12
  await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
12
13
  server.close((err) => (err ? reject(err) : resolve()));
13
14
  })));
14
15
  servers.length = 0;
16
+ for (const dir of tempDirs) {
17
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
18
+ }
19
+ tempDirs.length = 0;
15
20
  });
16
21
 
17
22
  async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> {
@@ -25,7 +30,9 @@ async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'
25
30
  return `http://${hostname}:${address.port}`;
26
31
  }
27
32
 
28
- describe('download helpers', () => {
33
+ // Windows Defender can briefly lock newly-written .tmp files, causing EPERM.
34
+ // Retry once to handle this flakiness.
35
+ describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
29
36
  it('resolves relative redirects against the original URL', () => {
30
37
  expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
31
38
  expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
@@ -45,7 +52,8 @@ describe('download helpers', () => {
45
52
  res.end();
46
53
  });
47
54
 
48
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
55
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
56
+ tempDirs.push(tempDir);
49
57
  const destPath = path.join(tempDir, 'file.txt');
50
58
  const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
51
59
 
@@ -71,7 +79,8 @@ describe('download helpers', () => {
71
79
  res.end();
72
80
  });
73
81
 
74
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
82
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
83
+ tempDirs.push(tempDir);
75
84
  const destPath = path.join(tempDir, 'redirect.txt');
76
85
  const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
77
86
 
@@ -94,7 +103,8 @@ describe('download helpers', () => {
94
103
  res.end();
95
104
  });
96
105
 
97
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
106
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
107
+ tempDirs.push(tempDir);
98
108
  const destPath = path.join(tempDir, 'redirect-header.txt');
99
109
  const result = await httpDownload(`${redirectUrl}/start`, destPath, {
100
110
  headers: { Cookie: 'sid=header-cookie' },