@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
@@ -14,14 +14,30 @@ import { fullName, getRegistry } from './registry.js';
14
14
  import { formatRegistryHelpText } from './serialization.js';
15
15
  import { render as renderOutput } from './output.js';
16
16
  import { executeCommand } from './execution.js';
17
- import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
17
+ import { CliError, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
18
+ import { checkDaemonStatus } from './browser/discover.js';
19
+ export function normalizeArgValue(argType, value, name) {
20
+ if (argType !== 'bool')
21
+ return value;
22
+ if (typeof value === 'boolean')
23
+ return value;
24
+ if (value == null || value === '')
25
+ return false;
26
+ const normalized = String(value).trim().toLowerCase();
27
+ if (normalized === 'true')
28
+ return true;
29
+ if (normalized === 'false')
30
+ return false;
31
+ throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
32
+ }
18
33
  /**
19
34
  * Register a single CliCommand as a Commander subcommand.
20
35
  */
21
36
  export function registerCommandToProgram(siteCmd, cmd) {
22
37
  if (siteCmd.commands.some((c) => c.name() === cmd.name))
23
38
  return;
24
- const subCmd = siteCmd.command(cmd.name).description(cmd.description);
39
+ const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
40
+ const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
25
41
  // Register positional args first, then named options
26
42
  const positionalArgs = [];
27
43
  for (const arg of cmd.args) {
@@ -48,27 +64,32 @@ export function registerCommandToProgram(siteCmd, cmd) {
48
64
  const actionOpts = actionArgs[positionalArgs.length] ?? {};
49
65
  const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts : {};
50
66
  const startTime = Date.now();
51
- // ── Collect kwargs ──────────────────────────────────────────────────
52
- const kwargs = {};
53
- for (let i = 0; i < positionalArgs.length; i++) {
54
- const v = actionArgs[i];
55
- if (v !== undefined)
56
- kwargs[positionalArgs[i].name] = v;
57
- }
58
- for (const arg of cmd.args) {
59
- if (arg.positional)
60
- continue;
61
- const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
62
- const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
63
- if (v !== undefined)
64
- kwargs[arg.name] = v;
65
- }
66
67
  // ── Execute + render ────────────────────────────────────────────────
67
68
  try {
69
+ // ── Collect kwargs ────────────────────────────────────────────────
70
+ const kwargs = {};
71
+ for (let i = 0; i < positionalArgs.length; i++) {
72
+ const v = actionArgs[i];
73
+ if (v !== undefined)
74
+ kwargs[positionalArgs[i].name] = v;
75
+ }
76
+ for (const arg of cmd.args) {
77
+ if (arg.positional)
78
+ continue;
79
+ const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
80
+ const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
81
+ if (v !== undefined)
82
+ kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
83
+ }
68
84
  const verbose = optionsRecord.verbose === true;
69
85
  const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
70
86
  if (verbose)
71
87
  process.env.OPENCLI_VERBOSE = '1';
88
+ if (cmd.deprecated) {
89
+ const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
90
+ const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
91
+ console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
92
+ }
72
93
  const result = await executeCommand(cmd, kwargs, verbose);
73
94
  if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
74
95
  console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
@@ -84,22 +105,148 @@ export function registerCommandToProgram(siteCmd, cmd) {
84
105
  });
85
106
  }
86
107
  catch (err) {
87
- if (err instanceof CliError) {
88
- const icon = ERROR_ICONS[err.code] ?? '⚠️';
89
- console.error(chalk.red(`${icon} ${err.message}`));
90
- if (err.hint)
91
- console.error(chalk.yellow(`→ ${err.hint}`));
92
- }
93
- else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
94
- console.error(chalk.red(err.stack));
95
- }
96
- else {
97
- console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
98
- }
108
+ await renderError(err, fullName(cmd), optionsRecord.verbose === true);
99
109
  process.exitCode = 1;
100
110
  }
101
111
  });
102
112
  }
113
+ // ── Error rendering ──────────────────────────────────────────────────────────
114
+ const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
115
+ /** Pattern-based classifier for untyped errors thrown by adapters. */
116
+ function classifyGenericError(msg) {
117
+ const m = msg.toLowerCase();
118
+ if (/not logged in|login required|please log in|未登录|请先登录|authentication required|cookie expired/.test(m))
119
+ return 'auth';
120
+ // Match "HTTP 404", "status: 500", "status 403", bare "404 Not Found", etc.
121
+ if (/\b(status[: ]+)?[45]\d{2}\b|http[/ ][45]\d{2}/.test(m))
122
+ return 'http';
123
+ if (/not found|未找到|could not find|no .+ found/.test(m))
124
+ return 'not-found';
125
+ return 'other';
126
+ }
127
+ /** Render a status line for BrowserConnectError based on real-time or kind-derived state. */
128
+ function renderBridgeStatus(running, extensionConnected) {
129
+ const ok = chalk.green('✓');
130
+ const fail = chalk.red('✗');
131
+ console.error(` Daemon ${running ? ok : fail} ${running ? 'running' : 'not running'}`);
132
+ console.error(` Extension ${extensionConnected ? ok : fail} ${extensionConnected ? 'connected' : 'not connected'}`);
133
+ console.error();
134
+ if (!running) {
135
+ console.error(chalk.yellow(' Run the command again — daemon should auto-start.'));
136
+ console.error(chalk.dim(' Still failing? Run: opencli doctor'));
137
+ }
138
+ else if (!extensionConnected) {
139
+ console.error(chalk.yellow(' Install the Browser Bridge extension to continue:'));
140
+ console.error(chalk.dim(' 1. Download from github.com/jackwener/opencli/releases'));
141
+ console.error(chalk.dim(' 2. chrome://extensions → Enable Developer Mode → Load unpacked'));
142
+ }
143
+ else {
144
+ console.error(chalk.yellow(' Connection failed despite extension being active.'));
145
+ console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
146
+ }
147
+ }
148
+ async function renderError(err, cmdName, verbose) {
149
+ // ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
150
+ if (err instanceof BrowserConnectError) {
151
+ console.error(chalk.red('🔌 Browser Bridge not connected'));
152
+ console.error();
153
+ try {
154
+ // 300ms matches execution.ts — localhost responds in <50ms when running.
155
+ const status = await checkDaemonStatus({ timeout: 300 });
156
+ renderBridgeStatus(status.running, status.extensionConnected);
157
+ }
158
+ catch (_statusErr) {
159
+ // checkDaemonStatus itself failed — derive best-guess state from kind.
160
+ const running = err.kind !== 'daemon-not-running';
161
+ const extensionConnected = err.kind === 'command-failed';
162
+ renderBridgeStatus(running, extensionConnected);
163
+ }
164
+ return;
165
+ }
166
+ // ── AuthRequiredError ─────────────────────────────────────────────────
167
+ if (err instanceof AuthRequiredError) {
168
+ console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
169
+ // Respect custom hints set by the adapter; fall back to generic guidance.
170
+ console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
171
+ return;
172
+ }
173
+ // ── TimeoutError ──────────────────────────────────────────────────────
174
+ if (err instanceof TimeoutError) {
175
+ console.error(chalk.red(`⏱ ${err.message}`));
176
+ console.error(chalk.yellow('→ Try again, or raise the limit:'));
177
+ console.error(chalk.dim(` OPENCLI_BROWSER_COMMAND_TIMEOUT=60 ${cmdName}`));
178
+ return;
179
+ }
180
+ // ── SelectorError / EmptyResultError: likely outdated adapter ─────────
181
+ if (err instanceof SelectorError || err instanceof EmptyResultError) {
182
+ const icon = ERROR_ICONS[err.code] ?? '⚠️';
183
+ console.error(chalk.red(`${icon} ${err.message}`));
184
+ console.error(chalk.yellow('→ The page structure may have changed — this adapter may be outdated.'));
185
+ console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
186
+ console.error(chalk.dim(` Report: ${ISSUES_URL}`));
187
+ return;
188
+ }
189
+ // ── ArgumentError ─────────────────────────────────────────────────────
190
+ if (err instanceof ArgumentError) {
191
+ console.error(chalk.red(`❌ ${err.message}`));
192
+ if (err.hint)
193
+ console.error(chalk.yellow(`→ ${err.hint}`));
194
+ return;
195
+ }
196
+ // ── AdapterLoadError ──────────────────────────────────────────────────
197
+ if (err instanceof AdapterLoadError) {
198
+ console.error(chalk.red(`📦 ${err.message}`));
199
+ if (err.hint)
200
+ console.error(chalk.yellow(`→ ${err.hint}`));
201
+ return;
202
+ }
203
+ // ── CommandExecutionError ─────────────────────────────────────────────
204
+ if (err instanceof CommandExecutionError) {
205
+ console.error(chalk.red(`💥 ${err.message}`));
206
+ if (err.hint) {
207
+ console.error(chalk.yellow(`→ ${err.hint}`));
208
+ }
209
+ else {
210
+ console.error(chalk.dim(` Add --verbose for details, or report: ${ISSUES_URL}`));
211
+ }
212
+ return;
213
+ }
214
+ // ── Other typed CliError (fallback for future codes) ──────────────────
215
+ if (err instanceof CliError) {
216
+ const icon = ERROR_ICONS[err.code] ?? '⚠️';
217
+ console.error(chalk.red(`${icon} ${err.message}`));
218
+ if (err.hint)
219
+ console.error(chalk.yellow(`→ ${err.hint}`));
220
+ return;
221
+ }
222
+ // ── Generic Error from adapters: classify by message pattern ──────────
223
+ const msg = getErrorMessage(err);
224
+ const kind = classifyGenericError(msg);
225
+ if (kind === 'auth') {
226
+ console.error(chalk.red(`🔒 ${msg}`));
227
+ console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
228
+ return;
229
+ }
230
+ if (kind === 'http') {
231
+ console.error(chalk.red(`🌐 ${msg}`));
232
+ console.error(chalk.yellow('→ Check your login status, or the site may be temporarily unavailable.'));
233
+ return;
234
+ }
235
+ if (kind === 'not-found') {
236
+ console.error(chalk.red(`📭 ${msg}`));
237
+ console.error(chalk.yellow('→ The resource was not found. The adapter or page structure may have changed.'));
238
+ console.error(chalk.dim(` Report: ${ISSUES_URL}`));
239
+ return;
240
+ }
241
+ // ── Unknown error: show stack in verbose mode ─────────────────────────
242
+ if (verbose && err instanceof Error && err.stack) {
243
+ console.error(chalk.red(err.stack));
244
+ }
245
+ else {
246
+ console.error(chalk.red(`💥 Unexpected error: ${msg}`));
247
+ console.error(chalk.dim(` Run with --verbose for details, or report: ${ISSUES_URL}`));
248
+ }
249
+ }
103
250
  /**
104
251
  * Register all commands from the registry onto a Commander program.
105
252
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Command } from 'commander';
3
+ const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
4
+ mockExecuteCommand: vi.fn(),
5
+ mockRenderOutput: vi.fn(),
6
+ }));
7
+ vi.mock('./execution.js', () => ({
8
+ executeCommand: mockExecuteCommand,
9
+ }));
10
+ vi.mock('./output.js', () => ({
11
+ render: mockRenderOutput,
12
+ }));
13
+ import { registerCommandToProgram } from './commanderAdapter.js';
14
+ describe('commanderAdapter arg passing', () => {
15
+ const cmd = {
16
+ site: 'paperreview',
17
+ name: 'submit',
18
+ description: 'Submit a PDF',
19
+ browser: false,
20
+ args: [
21
+ { name: 'pdf', positional: true, required: true, help: 'Path to the paper PDF' },
22
+ { name: 'dry-run', type: 'bool', default: false, help: 'Validate only' },
23
+ { name: 'prepare-only', type: 'bool', default: false, help: 'Prepare only' },
24
+ ],
25
+ func: vi.fn(),
26
+ };
27
+ beforeEach(() => {
28
+ mockExecuteCommand.mockReset();
29
+ mockExecuteCommand.mockResolvedValue([]);
30
+ mockRenderOutput.mockReset();
31
+ delete process.env.OPENCLI_VERBOSE;
32
+ process.exitCode = undefined;
33
+ });
34
+ it('passes bool flag values through to executeCommand for coercion', async () => {
35
+ const program = new Command();
36
+ const siteCmd = program.command('paperreview');
37
+ registerCommandToProgram(siteCmd, cmd);
38
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'false']);
39
+ expect(mockExecuteCommand).toHaveBeenCalled();
40
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
41
+ expect(kwargs.pdf).toBe('./paper.pdf');
42
+ expect(kwargs).toHaveProperty('dry-run');
43
+ });
44
+ it('passes valueless bool flags as true to executeCommand', async () => {
45
+ const program = new Command();
46
+ const siteCmd = program.command('paperreview');
47
+ registerCommandToProgram(siteCmd, cmd);
48
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--prepare-only']);
49
+ expect(mockExecuteCommand).toHaveBeenCalled();
50
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
51
+ expect(kwargs.pdf).toBe('./paper.pdf');
52
+ expect(kwargs['prepare-only']).toBe(true);
53
+ });
54
+ it('rejects invalid bool values before calling executeCommand', async () => {
55
+ const program = new Command();
56
+ const siteCmd = program.command('paperreview');
57
+ registerCommandToProgram(siteCmd, cmd);
58
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
59
+ // normalizeArgValue validates bools eagerly; executeCommand should not be reached
60
+ expect(mockExecuteCommand).not.toHaveBeenCalled();
61
+ });
62
+ });
package/dist/daemon.js CHANGED
@@ -25,6 +25,7 @@ const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_P
25
25
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
26
26
  // ─── State ───────────────────────────────────────────────────────────
27
27
  let extensionWs = null;
28
+ let extensionVersion = null;
28
29
  const pending = new Map();
29
30
  let idleTimer = null;
30
31
  const LOG_BUFFER_SIZE = 200;
@@ -103,6 +104,7 @@ async function handleRequest(req, res) {
103
104
  jsonResponse(res, 200, {
104
105
  ok: true,
105
106
  extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
107
+ extensionVersion,
106
108
  pending: pending.size,
107
109
  });
108
110
  return;
@@ -195,6 +197,11 @@ wss.on('connection', (ws) => {
195
197
  ws.on('message', (data) => {
196
198
  try {
197
199
  const msg = JSON.parse(data.toString());
200
+ // Handle hello message from extension (version handshake)
201
+ if (msg.type === 'hello') {
202
+ extensionVersion = typeof msg.version === 'string' ? msg.version : null;
203
+ return;
204
+ }
198
205
  // Handle log messages from extension
199
206
  if (msg.type === 'log') {
200
207
  const prefix = msg.level === 'error' ? '❌' : msg.level === 'warn' ? '⚠️' : '📋';
@@ -219,6 +226,7 @@ wss.on('connection', (ws) => {
219
226
  clearInterval(heartbeatInterval);
220
227
  if (extensionWs === ws) {
221
228
  extensionWs = null;
229
+ extensionVersion = null;
222
230
  // Reject all pending requests since the extension is gone
223
231
  for (const [id, p] of pending) {
224
232
  clearTimeout(p.timer);
@@ -229,8 +237,16 @@ wss.on('connection', (ws) => {
229
237
  });
230
238
  ws.on('error', () => {
231
239
  clearInterval(heartbeatInterval);
232
- if (extensionWs === ws)
240
+ if (extensionWs === ws) {
233
241
  extensionWs = null;
242
+ extensionVersion = null;
243
+ // Reject pending requests in case 'close' does not follow this 'error'
244
+ for (const [, p] of pending) {
245
+ clearTimeout(p.timer);
246
+ p.reject(new Error('Extension disconnected'));
247
+ }
248
+ pending.clear();
249
+ }
234
250
  });
235
251
  });
236
252
  // ─── Start ───────────────────────────────────────────────────────────
package/dist/discovery.js CHANGED
@@ -19,6 +19,7 @@ import { log } from './logger.js';
19
19
  export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
20
20
  /** Matches files that register commands via cli() or lifecycle hooks */
21
21
  const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
22
+ import { parseYamlArgs } from './yaml-schema.js';
22
23
  function parseStrategy(rawStrategy, fallback = Strategy.COOKIE) {
23
24
  if (!rawStrategy)
24
25
  return fallback;
@@ -71,6 +72,8 @@ async function loadFromManifest(manifestPath, clisDir) {
71
72
  pipeline: entry.pipeline,
72
73
  timeoutSeconds: entry.timeout,
73
74
  source: `manifest:${entry.site}/${entry.name}`,
75
+ deprecated: entry.deprecated,
76
+ replacedBy: entry.replacedBy,
74
77
  navigateBefore: entry.navigateBefore,
75
78
  };
76
79
  registerCommand(cmd);
@@ -91,6 +94,8 @@ async function loadFromManifest(manifestPath, clisDir) {
91
94
  columns: entry.columns,
92
95
  timeoutSeconds: entry.timeout,
93
96
  source: modulePath,
97
+ deprecated: entry.deprecated,
98
+ replacedBy: entry.replacedBy,
94
99
  navigateBefore: entry.navigateBefore,
95
100
  _lazy: true,
96
101
  _modulePath: modulePath,
@@ -122,22 +127,20 @@ async function discoverClisFromFs(dir) {
122
127
  const site = entry.name;
123
128
  const siteDir = path.join(dir, site);
124
129
  const files = await fs.promises.readdir(siteDir);
125
- const filePromises = [];
126
- for (const file of files) {
130
+ await Promise.all(files.map(async (file) => {
127
131
  const filePath = path.join(siteDir, file);
128
132
  if (file.endsWith('.yaml') || file.endsWith('.yml')) {
129
- filePromises.push(registerYamlCli(filePath, site));
133
+ await registerYamlCli(filePath, site);
130
134
  }
131
135
  else if ((file.endsWith('.js') && !file.endsWith('.d.js')) ||
132
136
  (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))) {
133
137
  if (!(await isCliModule(filePath)))
134
- continue;
135
- filePromises.push(import(pathToFileURL(filePath).href).catch((err) => {
138
+ return;
139
+ await import(pathToFileURL(filePath).href).catch((err) => {
136
140
  log.warn(`Failed to load module ${filePath}: ${getErrorMessage(err)}`);
137
- }));
141
+ });
138
142
  }
139
- }
140
- await Promise.all(filePromises);
143
+ }));
141
144
  });
142
145
  await Promise.all(sitePromises);
143
146
  }
@@ -153,20 +156,7 @@ async function registerYamlCli(filePath, defaultSite) {
153
156
  const strategyStr = cliDef.strategy ?? (cliDef.browser === false ? 'public' : 'cookie');
154
157
  const strategy = parseStrategy(strategyStr);
155
158
  const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
156
- const args = [];
157
- if (cliDef.args && typeof cliDef.args === 'object') {
158
- for (const [argName, argDef] of Object.entries(cliDef.args)) {
159
- args.push({
160
- name: argName,
161
- type: argDef?.type ?? 'str',
162
- default: argDef?.default,
163
- required: argDef?.required ?? false,
164
- positional: argDef?.positional ?? false,
165
- help: argDef?.description ?? argDef?.help ?? '',
166
- choices: argDef?.choices,
167
- });
168
- }
169
- }
159
+ const args = parseYamlArgs(cliDef.args);
170
160
  const cmd = {
171
161
  site,
172
162
  name,
@@ -179,6 +169,8 @@ async function registerYamlCli(filePath, defaultSite) {
179
169
  pipeline: cliDef.pipeline,
180
170
  timeoutSeconds: cliDef.timeout,
181
171
  source: filePath,
172
+ deprecated: cliDef.deprecated,
173
+ replacedBy: cliDef.replacedBy,
182
174
  navigateBefore: cliDef.navigateBefore,
183
175
  };
184
176
  registerCommand(cmd);
@@ -200,11 +192,12 @@ export async function discoverPlugins() {
200
192
  return;
201
193
  }
202
194
  const entries = await fs.promises.readdir(PLUGINS_DIR, { withFileTypes: true });
203
- for (const entry of entries) {
204
- if (!entry.isDirectory())
205
- continue;
206
- await discoverPluginDir(path.join(PLUGINS_DIR, entry.name), entry.name);
207
- }
195
+ await Promise.all(entries.map(async (entry) => {
196
+ const pluginDir = path.join(PLUGINS_DIR, entry.name);
197
+ if (!(await isDiscoverablePluginDir(entry, pluginDir)))
198
+ return;
199
+ await discoverPluginDir(pluginDir, entry.name);
200
+ }));
208
201
  }
209
202
  /**
210
203
  * Flat scan: read yaml/ts files directly in a plugin directory.
@@ -213,32 +206,29 @@ export async function discoverPlugins() {
213
206
  async function discoverPluginDir(dir, site) {
214
207
  const files = await fs.promises.readdir(dir);
215
208
  const fileSet = new Set(files);
216
- const promises = [];
217
- for (const file of files) {
209
+ await Promise.all(files.map(async (file) => {
218
210
  const filePath = path.join(dir, file);
219
211
  if (file.endsWith('.yaml') || file.endsWith('.yml')) {
220
- promises.push(registerYamlCli(filePath, site));
212
+ await registerYamlCli(filePath, site);
221
213
  }
222
214
  else if (file.endsWith('.js') && !file.endsWith('.d.js')) {
223
215
  if (!(await isCliModule(filePath)))
224
- continue;
225
- promises.push(import(pathToFileURL(filePath).href).catch((err) => {
216
+ return;
217
+ await import(pathToFileURL(filePath).href).catch((err) => {
226
218
  log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
227
- }));
219
+ });
228
220
  }
229
221
  else if (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts')) {
230
- // Skip .ts if a compiled .js sibling exists (production mode can't load .ts)
231
222
  const jsFile = file.replace(/\.ts$/, '.js');
223
+ // Prefer compiled .js — skip the .ts source file
232
224
  if (fileSet.has(jsFile))
233
- continue;
234
- if (!(await isCliModule(filePath)))
235
- continue;
236
- promises.push(import(pathToFileURL(filePath).href).catch((err) => {
237
- log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
238
- }));
225
+ return;
226
+ // No compiled .js found — cannot import raw .ts in production Node.js.
227
+ // This typically means esbuild transpilation failed during plugin install.
228
+ log.warn(`Plugin ${site}/${file}: no compiled .js found. ` +
229
+ `Run "opencli plugin update ${site}" to re-transpile, or install esbuild.`);
239
230
  }
240
- }
241
- await Promise.all(promises);
231
+ }));
242
232
  }
243
233
  async function isCliModule(filePath) {
244
234
  try {
@@ -250,3 +240,19 @@ async function isCliModule(filePath) {
250
240
  return false;
251
241
  }
252
242
  }
243
+ async function isDiscoverablePluginDir(entry, pluginDir) {
244
+ if (entry.isDirectory())
245
+ return true;
246
+ if (!entry.isSymbolicLink())
247
+ return false;
248
+ try {
249
+ return (await fs.promises.stat(pluginDir)).isDirectory();
250
+ }
251
+ catch (err) {
252
+ const code = err.code;
253
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
254
+ log.warn(`Failed to inspect plugin link ${pluginDir}: ${getErrorMessage(err)}`);
255
+ }
256
+ return false;
257
+ }
258
+ }
package/dist/doctor.d.ts CHANGED
@@ -1,11 +1,10 @@
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.
6
6
  */
7
7
  export type DoctorOptions = {
8
- fix?: boolean;
9
8
  yes?: boolean;
10
9
  live?: boolean;
11
10
  sessions?: boolean;
@@ -20,6 +19,7 @@ export type DoctorReport = {
20
19
  cliVersion?: string;
21
20
  daemonRunning: boolean;
22
21
  extensionConnected: boolean;
22
+ extensionVersion?: string;
23
23
  connectivity?: ConnectivityResult;
24
24
  sessions?: Array<{
25
25
  workspace: string;
package/dist/doctor.js 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.
@@ -10,6 +10,7 @@ import { checkDaemonStatus } from './browser/discover.js';
10
10
  import { BrowserBridge } from './browser/index.js';
11
11
  import { listSessions } from './browser/daemon-client.js';
12
12
  import { getErrorMessage } from './errors.js';
13
+ import { getRuntimeLabel } from './runtime-detect.js';
13
14
  /**
14
15
  * Test connectivity by attempting a real browser command.
15
16
  */
@@ -57,30 +58,36 @@ export async function runBrowserDoctor(opts = {}) {
57
58
  if (status.running && !status.extensionConnected) {
58
59
  issues.push('Daemon is running but the Chrome extension is not connected.\n' +
59
60
  'Please install the opencli Browser Bridge extension:\n' +
60
- ' 1. Download from GitHub Releases\n' +
61
+ ' 1. Download from https://github.com/jackwener/opencli/releases\n' +
61
62
  ' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
62
63
  ' 3. Click "Load unpacked" → select the extension folder');
63
64
  }
64
65
  if (connectivity && !connectivity.ok) {
65
66
  issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
66
67
  }
68
+ if (status.extensionVersion && opts.cliVersion && status.extensionVersion !== opts.cliVersion) {
69
+ issues.push(`Extension version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
70
+ ' Download the latest extension from: https://github.com/jackwener/opencli/releases');
71
+ }
67
72
  return {
68
73
  cliVersion: opts.cliVersion,
69
74
  daemonRunning: status.running,
70
75
  extensionConnected: status.extensionConnected,
76
+ extensionVersion: status.extensionVersion,
71
77
  connectivity,
72
78
  sessions,
73
79
  issues,
74
80
  };
75
81
  }
76
82
  export function renderBrowserDoctorReport(report) {
77
- const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
83
+ const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
78
84
  // Daemon status
79
85
  const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
80
86
  lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running'}`);
81
87
  // Extension status
82
88
  const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
83
- lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}`);
89
+ const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
90
+ lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
84
91
  // Connectivity
85
92
  if (report.connectivity) {
86
93
  const connIcon = report.connectivity.ok ? chalk.green('[OK]') : chalk.red('[FAIL]');