@jackwener/opencli 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
  2. package/.github/pull_request_template.md +7 -0
  3. package/.github/workflows/doc-check.yml +36 -0
  4. package/.github/workflows/docs.yml +7 -42
  5. package/CHANGELOG.md +23 -0
  6. package/CLI-EXPLORER.md +9 -8
  7. package/README.md +25 -10
  8. package/README.zh-CN.md +26 -11
  9. package/SKILL.md +95 -31
  10. package/dist/browser/cdp.js +6 -1
  11. package/dist/browser/page.d.ts +4 -1
  12. package/dist/browser/page.js +7 -1
  13. package/dist/build-manifest.js +23 -16
  14. package/dist/cli-manifest.json +431 -276
  15. package/dist/cli.d.ts +6 -0
  16. package/dist/cli.js +189 -162
  17. package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
  18. package/dist/clis/apple-podcasts/commands.test.js +76 -0
  19. package/dist/clis/apple-podcasts/search.js +2 -2
  20. package/dist/clis/apple-podcasts/top.js +9 -2
  21. package/dist/clis/arxiv/search.js +1 -1
  22. package/dist/clis/bilibili/dynamic.js +1 -1
  23. package/dist/clis/bilibili/favorite.js +1 -1
  24. package/dist/clis/bilibili/feed.js +1 -1
  25. package/dist/clis/bilibili/following.js +1 -1
  26. package/dist/clis/bilibili/history.js +1 -1
  27. package/dist/clis/bilibili/me.js +1 -1
  28. package/dist/clis/bilibili/ranking.js +1 -1
  29. package/dist/clis/bilibili/search.js +3 -3
  30. package/dist/clis/bilibili/subtitle.js +1 -1
  31. package/dist/clis/bilibili/user-videos.js +1 -1
  32. package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
  33. package/dist/clis/bloomberg/businessweek.js +17 -0
  34. package/dist/clis/bloomberg/economics.js +17 -0
  35. package/dist/clis/bloomberg/feeds.d.ts +1 -0
  36. package/dist/clis/bloomberg/feeds.js +15 -0
  37. package/dist/clis/bloomberg/industries.d.ts +1 -0
  38. package/dist/clis/bloomberg/industries.js +17 -0
  39. package/dist/clis/bloomberg/main.d.ts +1 -0
  40. package/dist/clis/bloomberg/main.js +17 -0
  41. package/dist/clis/bloomberg/markets.d.ts +1 -0
  42. package/dist/clis/bloomberg/markets.js +17 -0
  43. package/dist/clis/bloomberg/news.d.ts +1 -0
  44. package/dist/clis/bloomberg/news.js +105 -0
  45. package/dist/clis/bloomberg/opinions.d.ts +1 -0
  46. package/dist/clis/bloomberg/opinions.js +17 -0
  47. package/dist/clis/bloomberg/politics.d.ts +1 -0
  48. package/dist/clis/bloomberg/politics.js +17 -0
  49. package/dist/clis/bloomberg/tech.d.ts +1 -0
  50. package/dist/clis/bloomberg/tech.js +17 -0
  51. package/dist/clis/bloomberg/utils.d.ts +34 -0
  52. package/dist/clis/bloomberg/utils.js +364 -0
  53. package/dist/clis/bloomberg/utils.test.d.ts +1 -0
  54. package/dist/clis/bloomberg/utils.test.js +129 -0
  55. package/dist/clis/boss/batchgreet.js +2 -2
  56. package/dist/clis/boss/chatlist.js +2 -2
  57. package/dist/clis/boss/detail.js +2 -2
  58. package/dist/clis/boss/greet.js +4 -4
  59. package/dist/clis/boss/search.js +1 -1
  60. package/dist/clis/boss/send.js +1 -1
  61. package/dist/clis/boss/stats.js +2 -2
  62. package/dist/clis/chaoxing/assignments.js +1 -1
  63. package/dist/clis/chaoxing/exams.js +1 -1
  64. package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
  65. package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
  66. package/dist/clis/chaoxing/utils.test.d.ts +1 -0
  67. package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
  68. package/dist/clis/chatgpt/read.js +1 -1
  69. package/dist/clis/chatwise/export.js +1 -1
  70. package/dist/clis/chatwise/model.js +2 -2
  71. package/dist/clis/chatwise/screenshot.js +1 -1
  72. package/dist/clis/codex/export.js +1 -1
  73. package/dist/clis/codex/model.js +2 -2
  74. package/dist/clis/codex/screenshot.js +1 -1
  75. package/dist/clis/coupang/add-to-cart.js +3 -4
  76. package/dist/clis/coupang/search.js +2 -4
  77. package/dist/clis/coupang/utils.test.d.ts +1 -0
  78. package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
  79. package/dist/clis/ctrip/search.js +1 -1
  80. package/dist/clis/cursor/export.js +1 -1
  81. package/dist/clis/cursor/model.js +2 -2
  82. package/dist/clis/cursor/screenshot.js +1 -1
  83. package/dist/clis/jike/comment.js +2 -3
  84. package/dist/clis/jike/create.js +1 -2
  85. package/dist/clis/jike/feed.js +0 -1
  86. package/dist/clis/jike/like.js +1 -2
  87. package/dist/clis/jike/notifications.js +0 -1
  88. package/dist/clis/jike/post.yaml +1 -0
  89. package/dist/clis/jike/repost.js +1 -2
  90. package/dist/clis/jike/search.js +2 -3
  91. package/dist/clis/jike/topic.yaml +1 -0
  92. package/dist/clis/jike/user.yaml +1 -0
  93. package/dist/clis/jimeng/history.yaml +0 -1
  94. package/dist/clis/linkedin/search.js +7 -7
  95. package/dist/clis/linux-do/category.yaml +1 -0
  96. package/dist/clis/linux-do/search.yaml +4 -3
  97. package/dist/clis/linux-do/topic.yaml +1 -0
  98. package/dist/clis/notion/export.js +1 -1
  99. package/dist/clis/reddit/comment.js +3 -4
  100. package/dist/clis/reddit/read.js +4 -5
  101. package/dist/clis/reddit/save.js +2 -3
  102. package/dist/clis/reddit/saved.js +0 -1
  103. package/dist/clis/reddit/search.yaml +1 -0
  104. package/dist/clis/reddit/subscribe.js +0 -1
  105. package/dist/clis/reddit/upvote.js +2 -3
  106. package/dist/clis/reddit/upvoted.js +0 -1
  107. package/dist/clis/reddit/user-comments.yaml +1 -0
  108. package/dist/clis/reddit/user-posts.yaml +1 -0
  109. package/dist/clis/reddit/user.yaml +1 -0
  110. package/dist/clis/reuters/search.js +1 -1
  111. package/dist/clis/smzdm/search.js +2 -3
  112. package/dist/clis/stackoverflow/search.yaml +1 -0
  113. package/dist/clis/steam/top-sellers.yaml +29 -0
  114. package/dist/clis/twitter/accept.js +2 -2
  115. package/dist/clis/twitter/article.js +2 -2
  116. package/dist/clis/twitter/block.d.ts +1 -0
  117. package/dist/clis/twitter/block.js +88 -0
  118. package/dist/clis/twitter/delete.js +1 -1
  119. package/dist/clis/twitter/hide-reply.d.ts +1 -0
  120. package/dist/clis/twitter/hide-reply.js +66 -0
  121. package/dist/clis/twitter/like.js +1 -1
  122. package/dist/clis/twitter/post.js +1 -1
  123. package/dist/clis/twitter/reply-dm.js +1 -1
  124. package/dist/clis/twitter/reply.js +2 -2
  125. package/dist/clis/twitter/search.js +1 -1
  126. package/dist/clis/twitter/thread.js +2 -2
  127. package/dist/clis/twitter/trending.d.ts +1 -0
  128. package/dist/clis/twitter/trending.js +91 -0
  129. package/dist/clis/twitter/unblock.d.ts +1 -0
  130. package/dist/clis/twitter/unblock.js +71 -0
  131. package/dist/clis/v2ex/topic.yaml +1 -0
  132. package/dist/clis/weibo/hot.js +0 -1
  133. package/dist/clis/weread/book.js +1 -1
  134. package/dist/clis/weread/highlights.js +1 -1
  135. package/dist/clis/weread/notes.js +1 -1
  136. package/dist/clis/weread/search.js +1 -1
  137. package/dist/clis/wikipedia/search.js +1 -1
  138. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +15 -0
  139. package/dist/clis/xiaohongshu/creator-note-detail.js +69 -5
  140. package/dist/clis/xiaohongshu/creator-note-detail.test.js +80 -33
  141. package/dist/clis/xiaohongshu/creator-notes.js +35 -5
  142. package/dist/clis/xiaohongshu/creator-notes.test.js +35 -6
  143. package/dist/clis/xiaohongshu/creator-profile.js +0 -1
  144. package/dist/clis/xiaohongshu/creator-stats.js +0 -1
  145. package/dist/clis/xiaohongshu/download.js +2 -3
  146. package/dist/clis/xiaohongshu/feed.yaml +0 -1
  147. package/dist/clis/xiaohongshu/notifications.yaml +0 -1
  148. package/dist/clis/xiaohongshu/search.js +2 -2
  149. package/dist/clis/xiaohongshu/user.js +1 -2
  150. package/dist/clis/yahoo-finance/quote.js +0 -1
  151. package/dist/clis/youtube/search.js +1 -1
  152. package/dist/clis/youtube/transcript.js +1 -1
  153. package/dist/clis/youtube/video.js +1 -1
  154. package/dist/clis/zhihu/download.js +1 -2
  155. package/dist/clis/zhihu/question.js +1 -1
  156. package/dist/clis/zhihu/search.yaml +4 -3
  157. package/dist/commanderAdapter.d.ts +21 -0
  158. package/dist/commanderAdapter.js +111 -0
  159. package/dist/{engine.d.ts → discovery.d.ts} +0 -6
  160. package/dist/{engine.js → discovery.js} +1 -98
  161. package/dist/download/index.d.ts +2 -6
  162. package/dist/download/index.js +19 -46
  163. package/dist/engine.test.d.ts +1 -1
  164. package/dist/engine.test.js +8 -7
  165. package/dist/execution.d.ts +22 -0
  166. package/dist/execution.js +129 -0
  167. package/dist/explore.js +121 -107
  168. package/dist/external-clis.yaml +48 -0
  169. package/dist/external.d.ts +7 -2
  170. package/dist/external.js +11 -14
  171. package/dist/main.js +1 -1
  172. package/dist/pipeline/steps/browser.js +8 -2
  173. package/dist/registry.d.ts +2 -0
  174. package/dist/registry.js +2 -0
  175. package/dist/runtime.d.ts +5 -0
  176. package/dist/runtime.js +8 -0
  177. package/dist/serialization.d.ts +34 -0
  178. package/dist/serialization.js +63 -0
  179. package/dist/types.d.ts +4 -1
  180. package/docs/.vitepress/config.mts +14 -3
  181. package/docs/adapters/browser/arxiv.md +27 -0
  182. package/docs/adapters/browser/barchart.md +32 -0
  183. package/docs/adapters/browser/bloomberg.md +70 -0
  184. package/docs/adapters/browser/chaoxing.md +39 -0
  185. package/docs/adapters/browser/grok.md +35 -0
  186. package/docs/adapters/browser/hf.md +42 -0
  187. package/docs/adapters/browser/jike.md +45 -0
  188. package/docs/adapters/browser/jimeng.md +39 -0
  189. package/docs/adapters/browser/linux-do.md +45 -0
  190. package/docs/adapters/browser/sinafinance.md +35 -0
  191. package/docs/adapters/browser/stackoverflow.md +35 -0
  192. package/docs/adapters/browser/steam.md +26 -0
  193. package/docs/adapters/browser/twitter.md +3 -0
  194. package/docs/adapters/browser/weread.md +48 -0
  195. package/docs/adapters/browser/wikipedia.md +30 -0
  196. package/docs/adapters/browser/xiaohongshu.md +5 -1
  197. package/docs/adapters/desktop/chatgpt.md +3 -3
  198. package/docs/adapters/index.md +13 -0
  199. package/docs/advanced/download.md +4 -4
  200. package/docs/developer/architecture.md +17 -4
  201. package/package.json +1 -1
  202. package/scripts/check-doc-coverage.sh +69 -0
  203. package/scripts/copy-yaml.cjs +7 -0
  204. package/src/browser/cdp.ts +6 -1
  205. package/src/browser/page.ts +7 -1
  206. package/src/build-manifest.ts +25 -19
  207. package/src/cli.ts +218 -139
  208. package/src/clis/apple-podcasts/commands.test.ts +95 -0
  209. package/src/clis/apple-podcasts/search.ts +2 -2
  210. package/src/clis/apple-podcasts/top.ts +12 -2
  211. package/src/clis/arxiv/search.ts +1 -1
  212. package/src/clis/bilibili/dynamic.ts +1 -1
  213. package/src/clis/bilibili/favorite.ts +1 -1
  214. package/src/clis/bilibili/feed.ts +1 -1
  215. package/src/clis/bilibili/following.ts +1 -1
  216. package/src/clis/bilibili/history.ts +1 -1
  217. package/src/clis/bilibili/me.ts +1 -1
  218. package/src/clis/bilibili/ranking.ts +1 -1
  219. package/src/clis/bilibili/search.ts +3 -3
  220. package/src/clis/bilibili/subtitle.ts +1 -1
  221. package/src/clis/bilibili/user-videos.ts +1 -1
  222. package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
  223. package/src/clis/bloomberg/businessweek.ts +18 -0
  224. package/src/clis/bloomberg/economics.ts +18 -0
  225. package/src/clis/bloomberg/feeds.ts +16 -0
  226. package/src/clis/bloomberg/industries.ts +18 -0
  227. package/src/clis/bloomberg/main.ts +18 -0
  228. package/src/clis/bloomberg/markets.ts +18 -0
  229. package/src/clis/bloomberg/news.ts +136 -0
  230. package/src/clis/bloomberg/opinions.ts +18 -0
  231. package/src/clis/bloomberg/politics.ts +18 -0
  232. package/src/clis/bloomberg/tech.ts +18 -0
  233. package/src/clis/bloomberg/utils.test.ts +135 -0
  234. package/src/clis/bloomberg/utils.ts +429 -0
  235. package/src/clis/boss/batchgreet.ts +2 -2
  236. package/src/clis/boss/chatlist.ts +2 -2
  237. package/src/clis/boss/detail.ts +2 -2
  238. package/src/clis/boss/greet.ts +4 -4
  239. package/src/clis/boss/search.ts +1 -1
  240. package/src/clis/boss/send.ts +1 -1
  241. package/src/clis/boss/stats.ts +2 -2
  242. package/src/clis/chaoxing/assignments.ts +1 -1
  243. package/src/clis/chaoxing/exams.ts +1 -1
  244. package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
  245. package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
  246. package/src/clis/chatgpt/README.zh-CN.md +3 -3
  247. package/src/clis/chatgpt/read.ts +1 -1
  248. package/src/clis/chatwise/export.ts +1 -1
  249. package/src/clis/chatwise/model.ts +2 -2
  250. package/src/clis/chatwise/screenshot.ts +1 -1
  251. package/src/clis/codex/export.ts +1 -1
  252. package/src/clis/codex/model.ts +2 -2
  253. package/src/clis/codex/screenshot.ts +1 -1
  254. package/src/clis/coupang/add-to-cart.ts +3 -4
  255. package/src/clis/coupang/search.ts +2 -4
  256. package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
  257. package/src/clis/ctrip/search.ts +1 -1
  258. package/src/clis/cursor/export.ts +1 -1
  259. package/src/clis/cursor/model.ts +2 -2
  260. package/src/clis/cursor/screenshot.ts +1 -1
  261. package/src/clis/jike/comment.ts +2 -3
  262. package/src/clis/jike/create.ts +1 -2
  263. package/src/clis/jike/feed.ts +0 -1
  264. package/src/clis/jike/like.ts +1 -2
  265. package/src/clis/jike/notifications.ts +0 -1
  266. package/src/clis/jike/post.yaml +1 -0
  267. package/src/clis/jike/repost.ts +1 -2
  268. package/src/clis/jike/search.ts +2 -3
  269. package/src/clis/jike/topic.yaml +1 -0
  270. package/src/clis/jike/user.yaml +1 -0
  271. package/src/clis/jimeng/history.yaml +0 -1
  272. package/src/clis/linkedin/search.ts +7 -7
  273. package/src/clis/linux-do/category.yaml +1 -0
  274. package/src/clis/linux-do/search.yaml +4 -3
  275. package/src/clis/linux-do/topic.yaml +1 -0
  276. package/src/clis/notion/export.ts +1 -1
  277. package/src/clis/reddit/comment.ts +3 -4
  278. package/src/clis/reddit/read.ts +4 -5
  279. package/src/clis/reddit/save.ts +2 -3
  280. package/src/clis/reddit/saved.ts +0 -1
  281. package/src/clis/reddit/search.yaml +1 -0
  282. package/src/clis/reddit/subscribe.ts +0 -1
  283. package/src/clis/reddit/upvote.ts +2 -3
  284. package/src/clis/reddit/upvoted.ts +0 -1
  285. package/src/clis/reddit/user-comments.yaml +1 -0
  286. package/src/clis/reddit/user-posts.yaml +1 -0
  287. package/src/clis/reddit/user.yaml +1 -0
  288. package/src/clis/reuters/search.ts +1 -1
  289. package/src/clis/smzdm/search.ts +2 -3
  290. package/src/clis/stackoverflow/search.yaml +1 -0
  291. package/src/clis/steam/top-sellers.yaml +29 -0
  292. package/src/clis/twitter/accept.ts +2 -2
  293. package/src/clis/twitter/article.ts +2 -2
  294. package/src/clis/twitter/block.ts +92 -0
  295. package/src/clis/twitter/delete.ts +1 -1
  296. package/src/clis/twitter/hide-reply.ts +70 -0
  297. package/src/clis/twitter/like.ts +1 -1
  298. package/src/clis/twitter/post.ts +1 -1
  299. package/src/clis/twitter/reply-dm.ts +1 -1
  300. package/src/clis/twitter/reply.ts +2 -2
  301. package/src/clis/twitter/search.ts +1 -1
  302. package/src/clis/twitter/thread.ts +2 -2
  303. package/src/clis/twitter/trending.ts +113 -0
  304. package/src/clis/twitter/unblock.ts +75 -0
  305. package/src/clis/v2ex/topic.yaml +1 -0
  306. package/src/clis/weibo/hot.ts +0 -1
  307. package/src/clis/weread/book.ts +1 -1
  308. package/src/clis/weread/highlights.ts +1 -1
  309. package/src/clis/weread/notes.ts +1 -1
  310. package/src/clis/weread/search.ts +1 -1
  311. package/src/clis/wikipedia/search.ts +1 -1
  312. package/src/clis/xiaohongshu/creator-note-detail.test.ts +82 -33
  313. package/src/clis/xiaohongshu/creator-note-detail.ts +89 -5
  314. package/src/clis/xiaohongshu/creator-notes.test.ts +39 -6
  315. package/src/clis/xiaohongshu/creator-notes.ts +44 -5
  316. package/src/clis/xiaohongshu/creator-profile.ts +0 -1
  317. package/src/clis/xiaohongshu/creator-stats.ts +0 -1
  318. package/src/clis/xiaohongshu/download.ts +2 -3
  319. package/src/clis/xiaohongshu/feed.yaml +0 -1
  320. package/src/clis/xiaohongshu/notifications.yaml +0 -1
  321. package/src/clis/xiaohongshu/search.ts +2 -2
  322. package/src/clis/xiaohongshu/user.ts +1 -2
  323. package/src/clis/yahoo-finance/quote.ts +0 -1
  324. package/src/clis/youtube/search.ts +1 -1
  325. package/src/clis/youtube/transcript.ts +1 -1
  326. package/src/clis/youtube/video.ts +1 -1
  327. package/src/clis/zhihu/download.ts +1 -2
  328. package/src/clis/zhihu/question.ts +1 -1
  329. package/src/clis/zhihu/search.yaml +4 -3
  330. package/src/commanderAdapter.ts +113 -0
  331. package/src/{engine.ts → discovery.ts} +1 -108
  332. package/src/download/index.ts +21 -54
  333. package/src/engine.test.ts +8 -7
  334. package/src/execution.ts +138 -0
  335. package/src/explore.ts +135 -109
  336. package/src/external-clis.yaml +9 -0
  337. package/src/external.ts +15 -12
  338. package/src/main.ts +1 -1
  339. package/src/pipeline/steps/browser.ts +7 -2
  340. package/src/registry.ts +5 -0
  341. package/src/runtime.ts +9 -0
  342. package/src/serialization.ts +79 -0
  343. package/src/types.ts +1 -1
  344. package/tests/e2e/browser-public.test.ts +25 -0
  345. package/tests/e2e/public-commands.test.ts +55 -1
  346. package/dist/clis/twitter/trending.yaml +0 -46
  347. package/docs/public/CNAME +0 -1
  348. package/src/clis/twitter/trending.yaml +0 -46
  349. /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
  350. /package/dist/{chaoxing.test.d.ts → clis/bloomberg/businessweek.d.ts} +0 -0
  351. /package/dist/{coupang.test.d.ts → clis/bloomberg/economics.d.ts} +0 -0
  352. /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
  353. /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
  354. /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
package/dist/cli.d.ts CHANGED
@@ -1 +1,7 @@
1
+ /**
2
+ * CLI entry point: registers built-in commands and wires up Commander.
3
+ *
4
+ * Built-in commands are registered inline here (list, validate, explore, etc.).
5
+ * Dynamic adapter commands are registered via commanderAdapter.ts.
6
+ */
1
7
  export declare function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void;
package/dist/cli.js CHANGED
@@ -1,42 +1,61 @@
1
+ /**
2
+ * CLI entry point: registers built-in commands and wires up Commander.
3
+ *
4
+ * Built-in commands are registered inline here (list, validate, explore, etc.).
5
+ * Dynamic adapter commands are registered via commanderAdapter.ts.
6
+ */
1
7
  import { Command } from 'commander';
2
8
  import chalk from 'chalk';
3
- import { executeCommand } from './engine.js';
4
- import { Strategy, fullName, getRegistry, strategyLabel } from './registry.js';
9
+ import { fullName, getRegistry, strategyLabel } from './registry.js';
10
+ import { serializeCommand, formatArgSummary } from './serialization.js';
5
11
  import { render as renderOutput } from './output.js';
6
- import { BrowserBridge, CDPBridge } from './browser/index.js';
7
- import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
12
+ import { getBrowserFactory, browserSession } from './runtime.js';
8
13
  import { PKG_VERSION } from './version.js';
9
14
  import { printCompletionScript } from './completion.js';
10
- import { CliError } from './errors.js';
11
- import { shouldUseBrowserSession } from './capabilityRouting.js';
12
15
  import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
16
+ import { registerAllCommands } from './commanderAdapter.js';
13
17
  export function runCli(BUILTIN_CLIS, USER_CLIS) {
14
18
  const program = new Command();
15
- program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
16
- // ── Built-in commands ──────────────────────────────────────────────────────
17
- program.command('list').description('List all available CLI commands').option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('--json', 'JSON output (deprecated)')
19
+ // enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
20
+ // prerequisite for passThroughOptions to forward --help/--version to external binaries
21
+ program
22
+ .name('opencli')
23
+ .description('Make any website your CLI. Zero setup. AI-powered.')
24
+ .version(PKG_VERSION)
25
+ .enablePositionalOptions();
26
+ // ── Built-in: list ────────────────────────────────────────────────────────
27
+ program
28
+ .command('list')
29
+ .description('List all available CLI commands')
30
+ .option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
31
+ .option('--json', 'JSON output (deprecated)')
18
32
  .action((opts) => {
19
33
  const registry = getRegistry();
20
34
  const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b)));
21
- const rows = commands.map(c => ({
22
- command: fullName(c),
23
- site: c.site,
24
- name: c.name,
25
- description: c.description,
26
- strategy: strategyLabel(c),
27
- browser: c.browser,
28
- args: c.args.map(a => a.name).join(', '),
29
- }));
30
35
  const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format;
36
+ const isStructured = fmt === 'json' || fmt === 'yaml';
31
37
  if (fmt !== 'table') {
38
+ const rows = isStructured
39
+ ? commands.map(serializeCommand)
40
+ : commands.map(c => ({
41
+ command: fullName(c),
42
+ site: c.site,
43
+ name: c.name,
44
+ description: c.description,
45
+ strategy: strategyLabel(c),
46
+ browser: !!c.browser,
47
+ args: formatArgSummary(c.args),
48
+ }));
32
49
  renderOutput(rows, {
33
50
  fmt,
34
- columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'],
51
+ columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args',
52
+ ...(isStructured ? ['columns', 'domain'] : [])],
35
53
  title: 'opencli/list',
36
54
  source: 'opencli list',
37
55
  });
38
56
  return;
39
57
  }
58
+ // Table (default) — grouped by site
40
59
  const sites = new Map();
41
60
  for (const cmd of commands) {
42
61
  const g = sites.get(cmd.site) ?? [];
@@ -49,14 +68,16 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
49
68
  for (const [site, cmds] of sites) {
50
69
  console.log(chalk.bold.cyan(` ${site}`));
51
70
  for (const cmd of cmds) {
52
- const tag = strategyLabel(cmd) === 'public' ? chalk.green('[public]') : chalk.yellow(`[${strategyLabel(cmd)}]`);
71
+ const tag = strategyLabel(cmd) === 'public'
72
+ ? chalk.green('[public]')
73
+ : chalk.yellow(`[${strategyLabel(cmd)}]`);
53
74
  console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`);
54
75
  }
55
76
  console.log();
56
77
  }
57
78
  const externalClis = loadExternalClis();
58
79
  if (externalClis.length > 0) {
59
- console.log(chalk.bold.cyan(` external CLIs`));
80
+ console.log(chalk.bold.cyan(' external CLIs'));
60
81
  for (const ext of externalClis) {
61
82
  const isInstalled = isBinaryInstalled(ext.binary);
62
83
  const tag = isInstalled ? chalk.green('[installed]') : chalk.yellow('[auto-install]');
@@ -67,40 +88,93 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
67
88
  console.log(chalk.dim(` ${commands.length} built-in commands across ${sites.size} sites, ${externalClis.length} external CLIs`));
68
89
  console.log();
69
90
  });
70
- program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name')
91
+ // ── Built-in: validate / verify ───────────────────────────────────────────
92
+ program
93
+ .command('validate')
94
+ .description('Validate CLI definitions')
95
+ .argument('[target]', 'site or site/name')
71
96
  .action(async (target) => {
72
97
  const { validateClisWithTarget, renderValidationReport } = await import('./validate.js');
73
98
  console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target)));
74
99
  });
75
- program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false)
100
+ program
101
+ .command('verify')
102
+ .description('Validate + smoke test')
103
+ .argument('[target]')
104
+ .option('--smoke', 'Run smoke tests', false)
76
105
  .action(async (target, opts) => {
77
106
  const { verifyClis, renderVerifyReport } = await import('./verify.js');
78
107
  const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
79
108
  console.log(renderVerifyReport(r));
80
109
  process.exitCode = r.ok ? 0 : 1;
81
110
  });
82
- program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
83
- .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `explore:${opts.site ?? (() => { try {
84
- return new URL(url).host;
85
- }
86
- catch {
87
- return 'default';
88
- } })()}`; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels, workspace }))); });
89
- program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
90
- .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
91
- program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
92
- .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `generate:${opts.site ?? (() => { try {
93
- return new URL(url).host;
94
- }
95
- catch {
96
- return 'default';
97
- } })()}`; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site, workspace }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
98
- program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
111
+ // ── Built-in: explore / synthesize / generate / cascade ───────────────────
112
+ program
113
+ .command('explore')
114
+ .alias('probe')
115
+ .description('Explore a website: discover APIs, stores, and recommend strategies')
116
+ .argument('<url>')
117
+ .option('--site <name>')
118
+ .option('--goal <text>')
119
+ .option('--wait <s>', '', '3')
120
+ .option('--auto', 'Enable interactive fuzzing')
121
+ .option('--click <labels>', 'Comma-separated labels to click before fuzzing')
122
+ .action(async (url, opts) => {
123
+ const { exploreUrl, renderExploreSummary } = await import('./explore.js');
124
+ const clickLabels = opts.click
125
+ ? opts.click.split(',').map((s) => s.trim())
126
+ : undefined;
127
+ const workspace = `explore:${inferHost(url, opts.site)}`;
128
+ const result = await exploreUrl(url, {
129
+ BrowserFactory: getBrowserFactory(),
130
+ site: opts.site,
131
+ goal: opts.goal,
132
+ waitSeconds: parseFloat(opts.wait),
133
+ auto: opts.auto,
134
+ clickLabels,
135
+ workspace,
136
+ });
137
+ console.log(renderExploreSummary(result));
138
+ });
139
+ program
140
+ .command('synthesize')
141
+ .description('Synthesize CLIs from explore')
142
+ .argument('<target>')
143
+ .option('--top <n>', '', '3')
144
+ .action(async (target, opts) => {
145
+ const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js');
146
+ console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) })));
147
+ });
148
+ program
149
+ .command('generate')
150
+ .description('One-shot: explore → synthesize → register')
151
+ .argument('<url>')
152
+ .option('--goal <text>')
153
+ .option('--site <name>')
154
+ .action(async (url, opts) => {
155
+ const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
156
+ const workspace = `generate:${inferHost(url, opts.site)}`;
157
+ const r = await generateCliFromUrl({
158
+ url,
159
+ BrowserFactory: getBrowserFactory(),
160
+ builtinClis: BUILTIN_CLIS,
161
+ userClis: USER_CLIS,
162
+ goal: opts.goal,
163
+ site: opts.site,
164
+ workspace,
165
+ });
166
+ console.log(renderGenerateSummary(r));
167
+ process.exitCode = r.ok ? 0 : 1;
168
+ });
169
+ program
170
+ .command('cascade')
171
+ .description('Strategy cascade: find simplest working strategy')
172
+ .argument('<url>')
173
+ .option('--site <name>')
99
174
  .action(async (url, opts) => {
100
175
  const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
101
- const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
102
- const result = await browserSession(BrowserFactory, async (page) => {
103
- // Navigate to the site first for cookie context
176
+ const workspace = `cascade:${inferHost(url, opts.site)}`;
177
+ const result = await browserSession(getBrowserFactory(), async (page) => {
104
178
  try {
105
179
  const siteUrl = new URL(url);
106
180
  await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
@@ -108,15 +182,12 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
108
182
  }
109
183
  catch { }
110
184
  return cascadeProbe(page, url);
111
- }, { workspace: `cascade:${opts.site ?? (() => { try {
112
- return new URL(url).host;
113
- }
114
- catch {
115
- return 'default';
116
- } })()}` });
185
+ }, { workspace });
117
186
  console.log(renderCascadeResult(result));
118
187
  });
119
- program.command('doctor')
188
+ // ── Built-in: doctor / setup / completion ─────────────────────────────────
189
+ program
190
+ .command('doctor')
120
191
  .description('Diagnose opencli browser bridge connectivity')
121
192
  .option('--live', 'Test browser connectivity (requires Chrome running)', false)
122
193
  .option('--sessions', 'Show active automation sessions', false)
@@ -125,20 +196,24 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
125
196
  const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
126
197
  console.log(renderBrowserDoctorReport(report));
127
198
  });
128
- program.command('setup')
199
+ program
200
+ .command('setup')
129
201
  .description('Interactive setup: verify browser bridge connectivity')
130
202
  .action(async () => {
131
203
  const { runSetup } = await import('./setup.js');
132
204
  await runSetup({ cliVersion: PKG_VERSION });
133
205
  });
134
- program.command('completion')
206
+ program
207
+ .command('completion')
135
208
  .description('Output shell completion script')
136
209
  .argument('<shell>', 'Shell type: bash, zsh, or fish')
137
210
  .action((shell) => {
138
211
  printCompletionScript(shell);
139
212
  });
213
+ // ── External CLIs ─────────────────────────────────────────────────────────
140
214
  const externalClis = loadExternalClis();
141
- program.command('install')
215
+ program
216
+ .command('install')
142
217
  .description('Install an external CLI')
143
218
  .argument('<name>', 'Name of the external CLI')
144
219
  .action((name) => {
@@ -150,138 +225,90 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
150
225
  }
151
226
  installExternalCli(ext);
152
227
  });
153
- program.command('register')
228
+ program
229
+ .command('register')
154
230
  .description('Register an external CLI')
155
231
  .argument('<name>', 'Name of the CLI')
156
232
  .option('--binary <bin>', 'Binary name if different from name')
157
233
  .option('--install <cmd>', 'Auto-install command')
158
234
  .option('--desc <text>', 'Description')
159
235
  .action((name, opts) => {
160
- registerExternalCli(name, opts.binary, opts.install, opts.desc);
236
+ registerExternalCli(name, { binary: opts.binary, install: opts.install, description: opts.desc });
161
237
  });
238
+ function passthroughExternal(name, parsedArgs) {
239
+ const args = parsedArgs ?? (() => {
240
+ const idx = process.argv.indexOf(name);
241
+ return process.argv.slice(idx + 1);
242
+ })();
243
+ try {
244
+ executeExternalCli(name, args, externalClis);
245
+ }
246
+ catch (err) {
247
+ console.error(chalk.red(`Error: ${err.message}`));
248
+ process.exitCode = 1;
249
+ }
250
+ }
162
251
  for (const ext of externalClis) {
163
252
  if (program.commands.some(c => c.name() === ext.name))
164
253
  continue;
165
- program.command(ext.name)
254
+ program
255
+ .command(ext.name)
166
256
  .description(`(External) ${ext.description || ext.name}`)
257
+ .argument('[args...]')
167
258
  .allowUnknownOption()
168
- .action(() => {
169
- // Retrieve args passed to the external CLI
170
- // Commander consumes standard args before the action, so we must slice process.argv directly.
171
- const extIndex = process.argv.indexOf(ext.name);
172
- const args = process.argv.slice(extIndex + 1);
173
- executeExternalCli(ext.name, args).catch(err => {
174
- console.error(chalk.red(`Error: ${err.message}`));
175
- process.exitCode = 1;
176
- });
177
- });
259
+ .passThroughOptions()
260
+ .helpOption(false)
261
+ .action((args) => passthroughExternal(ext.name, args));
178
262
  }
179
- // ── Antigravity serve (built-in, long-running) ──────────────────────────────
263
+ // ── Antigravity serve (long-running, special case) ────────────────────────
180
264
  const antigravityCmd = program.command('antigravity').description('antigravity commands');
181
- antigravityCmd.command('serve')
265
+ antigravityCmd
266
+ .command('serve')
182
267
  .description('Start Anthropic-compatible API proxy for Antigravity')
183
268
  .option('--port <port>', 'Server port (default: 8082)', '8082')
184
269
  .action(async (opts) => {
185
270
  const { startServe } = await import('./clis/antigravity/serve.js');
186
271
  await startServe({ port: parseInt(opts.port) });
187
272
  });
188
- // ── Dynamic site commands ──────────────────────────────────────────────────
189
- const registry = getRegistry();
273
+ // ── Dynamic adapter commands ──────────────────────────────────────────────
190
274
  const siteGroups = new Map();
191
- // Pre-seed with the antigravity command registered above to avoid duplicates
192
275
  siteGroups.set('antigravity', antigravityCmd);
193
- for (const [, cmd] of registry) {
194
- let siteCmd = siteGroups.get(cmd.site);
195
- if (!siteCmd) {
196
- siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
197
- siteGroups.set(cmd.site, siteCmd);
276
+ registerAllCommands(program, siteGroups);
277
+ // ── Unknown command fallback ──────────────────────────────────────────────
278
+ const DENY_LIST = new Set([
279
+ 'rm', 'sudo', 'dd', 'mkfs', 'fdisk', 'shutdown', 'reboot',
280
+ 'kill', 'killall', 'chmod', 'chown', 'passwd', 'su', 'mount',
281
+ 'umount', 'format', 'diskutil',
282
+ ]);
283
+ program.on('command:*', (operands) => {
284
+ const binary = operands[0];
285
+ if (DENY_LIST.has(binary)) {
286
+ console.error(chalk.red(`Refusing to register system command '${binary}'.`));
287
+ process.exitCode = 1;
288
+ return;
198
289
  }
199
- // Skip if this subcommand was already hardcoded (e.g. antigravity serve)
200
- if (siteCmd.commands.some((c) => c.name() === cmd.name))
201
- continue;
202
- const subCmd = siteCmd.command(cmd.name).description(cmd.description);
203
- // Register positional args first, then named options
204
- const positionalArgs = [];
205
- for (const arg of cmd.args) {
206
- if (arg.positional) {
207
- const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
208
- subCmd.argument(bracket, arg.help ?? '');
209
- positionalArgs.push(arg);
210
- }
211
- else {
212
- const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
213
- if (arg.required)
214
- subCmd.requiredOption(flag, arg.help ?? '');
215
- else if (arg.default != null)
216
- subCmd.option(flag, arg.help ?? '', String(arg.default));
217
- else
218
- subCmd.option(flag, arg.help ?? '');
219
- }
290
+ if (isBinaryInstalled(binary)) {
291
+ console.log(chalk.cyan(`🔹 Auto-discovered local CLI '${binary}'. Registering...`));
292
+ registerExternalCli(binary);
293
+ passthroughExternal(binary);
220
294
  }
221
- subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
222
- subCmd.action(async (...actionArgs) => {
223
- // Commander passes positional args first, then options object, then the Command
224
- const actionOpts = actionArgs[positionalArgs.length] ?? {};
225
- const startTime = Date.now();
226
- const kwargs = {};
227
- // Collect positional args
228
- for (let i = 0; i < positionalArgs.length; i++) {
229
- const arg = positionalArgs[i];
230
- const v = actionArgs[i];
231
- if (v !== undefined)
232
- kwargs[arg.name] = v;
233
- }
234
- // Collect named options
235
- for (const arg of cmd.args) {
236
- if (arg.positional)
237
- continue;
238
- const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
239
- const v = actionOpts[arg.name] ?? actionOpts[camelName];
240
- if (v !== undefined)
241
- kwargs[arg.name] = v;
242
- }
243
- try {
244
- if (actionOpts.verbose)
245
- process.env.OPENCLI_VERBOSE = '1';
246
- let result;
247
- if (shouldUseBrowserSession(cmd)) {
248
- const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
249
- result = await browserSession(BrowserFactory, async (page) => {
250
- // Cookie/header strategies require same-origin context for credentialed fetch.
251
- if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
252
- try {
253
- await page.goto(`https://${cmd.domain}`);
254
- await page.wait(2);
255
- }
256
- catch { }
257
- }
258
- return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
259
- }, { workspace: `site:${cmd.site}` });
260
- }
261
- else {
262
- result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
263
- }
264
- if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
265
- console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
266
- }
267
- const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
268
- renderOutput(result, { fmt: actionOpts.format, columns: resolved.columns, title: `${resolved.site}/${resolved.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(resolved), footerExtra: resolved.footerExtra?.(kwargs) });
269
- }
270
- catch (err) {
271
- if (err instanceof CliError) {
272
- console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
273
- if (err.hint)
274
- console.error(chalk.yellow(`Hint: ${err.hint}`));
275
- }
276
- else if (actionOpts.verbose && err.stack) {
277
- console.error(chalk.red(err.stack));
278
- }
279
- else {
280
- console.error(chalk.red(`Error: ${err.message ?? err}`));
281
- }
282
- process.exitCode = 1;
283
- }
284
- });
285
- }
295
+ else {
296
+ console.error(chalk.red(`error: unknown command '${binary}'`));
297
+ program.outputHelp();
298
+ process.exitCode = 1;
299
+ }
300
+ });
286
301
  program.parse();
287
302
  }
303
+ // ── Helpers ─────────────────────────────────────────────────────────────────
304
+ /** Infer a workspace-friendly hostname from a URL, with site override. */
305
+ function inferHost(url, site) {
306
+ if (site)
307
+ return site;
308
+ try {
309
+ return new URL(url).host;
310
+ }
311
+ catch {
312
+ return 'default';
313
+ }
314
+ }
@@ -0,0 +1,2 @@
1
+ import './search.js';
2
+ import './top.js';
@@ -0,0 +1,76 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './search.js';
4
+ import './top.js';
5
+ describe('apple-podcasts search command', () => {
6
+ beforeEach(() => {
7
+ vi.restoreAllMocks();
8
+ });
9
+ it('uses the positional query argument for the iTunes search request', async () => {
10
+ const cmd = getRegistry().get('apple-podcasts/search');
11
+ expect(cmd?.func).toBeTypeOf('function');
12
+ const fetchMock = vi.fn().mockResolvedValue({
13
+ ok: true,
14
+ json: () => Promise.resolve({
15
+ results: [
16
+ {
17
+ collectionId: 42,
18
+ collectionName: 'Machine Learning Guide',
19
+ artistName: 'OpenCLI',
20
+ trackCount: 12,
21
+ primaryGenreName: 'Technology',
22
+ },
23
+ ],
24
+ }),
25
+ });
26
+ vi.stubGlobal('fetch', fetchMock);
27
+ const result = await cmd.func(null, {
28
+ query: 'machine learning',
29
+ keyword: 'sports',
30
+ limit: 5,
31
+ });
32
+ expect(fetchMock).toHaveBeenCalledWith('https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5');
33
+ expect(result).toEqual([
34
+ {
35
+ id: 42,
36
+ title: 'Machine Learning Guide',
37
+ author: 'OpenCLI',
38
+ episodes: 12,
39
+ genre: 'Technology',
40
+ },
41
+ ]);
42
+ });
43
+ });
44
+ describe('apple-podcasts top command', () => {
45
+ beforeEach(() => {
46
+ vi.restoreAllMocks();
47
+ });
48
+ it('uses the canonical Apple charts host and maps ranked results', async () => {
49
+ const cmd = getRegistry().get('apple-podcasts/top');
50
+ expect(cmd?.func).toBeTypeOf('function');
51
+ const fetchMock = vi.fn().mockResolvedValue({
52
+ ok: true,
53
+ json: () => Promise.resolve({
54
+ feed: {
55
+ results: [
56
+ { id: '100', name: 'Top Show', artistName: 'Host A' },
57
+ { id: '101', name: 'Second Show', artistName: 'Host B' },
58
+ ],
59
+ },
60
+ }),
61
+ });
62
+ vi.stubGlobal('fetch', fetchMock);
63
+ const result = await cmd.func(null, { country: 'US', limit: 2 });
64
+ expect(fetchMock).toHaveBeenCalledWith('https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json');
65
+ expect(result).toEqual([
66
+ { rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
67
+ { rank: 2, title: 'Second Show', author: 'Host B', id: '101' },
68
+ ]);
69
+ });
70
+ it('normalizes network failures into CliError output', async () => {
71
+ const cmd = getRegistry().get('apple-podcasts/top');
72
+ expect(cmd?.func).toBeTypeOf('function');
73
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
74
+ await expect(cmd.func(null, { country: 'us', limit: 3 })).rejects.toThrow('Unable to reach Apple Podcasts charts for US');
75
+ });
76
+ });
@@ -8,12 +8,12 @@ cli({
8
8
  strategy: Strategy.PUBLIC,
9
9
  browser: false,
10
10
  args: [
11
- { name: 'keyword', positional: true, required: true, help: 'Search keyword' },
11
+ { name: 'query', positional: true, required: true, help: 'Search keyword' },
12
12
  { name: 'limit', type: 'int', default: 10, help: 'Max results' },
13
13
  ],
14
14
  columns: ['id', 'title', 'author', 'episodes', 'genre'],
15
15
  func: async (_page, args) => {
16
- const term = encodeURIComponent(args.keyword);
16
+ const term = encodeURIComponent(args.query);
17
17
  const limit = Math.max(1, Math.min(Number(args.limit), 25));
18
18
  const data = await itunesFetch(`/search?term=${term}&media=podcast&limit=${limit}`);
19
19
  if (!data.results?.length)
@@ -1,7 +1,7 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import { CliError } from '../../errors.js';
3
3
  // Apple Marketing Tools RSS API — public, no key required
4
- const CHARTS_URL = 'https://rss.applemarketingtools.com/api/v2';
4
+ const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
5
5
  cli({
6
6
  site: 'apple-podcasts',
7
7
  name: 'top',
@@ -17,7 +17,14 @@ cli({
17
17
  const limit = Math.max(1, Math.min(Number(args.limit), 100));
18
18
  const country = String(args.country || 'us').trim().toLowerCase();
19
19
  const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
20
- const resp = await fetch(url);
20
+ let resp;
21
+ try {
22
+ resp = await fetch(url);
23
+ }
24
+ catch (error) {
25
+ const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
26
+ throw new CliError('FETCH_ERROR', `Unable to reach Apple Podcasts charts for ${country.toUpperCase()}`, `Apple charts may be temporarily unavailable (${reason}). Try again later.`);
27
+ }
21
28
  if (!resp.ok)
22
29
  throw new CliError('FETCH_ERROR', `Charts API HTTP ${resp.status}`, `Check country code: ${country}`);
23
30
  const data = await resp.json();
@@ -8,7 +8,7 @@ cli({
8
8
  strategy: Strategy.PUBLIC,
9
9
  browser: false,
10
10
  args: [
11
- { name: 'keyword', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
11
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
12
12
  { name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
13
13
  ],
14
14
  columns: ['id', 'title', 'authors', 'published'],
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { apiGet } from '../../bilibili.js';
2
+ import { apiGet } from './utils.js';
3
3
  cli({
4
4
  site: 'bilibili',
5
5
  name: 'dynamic',
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { apiGet, payloadData } from '../../bilibili.js';
2
+ import { apiGet, payloadData } from './utils.js';
3
3
  cli({
4
4
  site: 'bilibili',
5
5
  name: 'favorite',
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { apiGet, payloadData, stripHtml } from '../../bilibili.js';
2
+ import { apiGet, payloadData, stripHtml } from './utils.js';
3
3
  cli({
4
4
  site: 'bilibili',
5
5
  name: 'feed',
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { fetchJson, getSelfUid, resolveUid } from '../../bilibili.js';
2
+ import { fetchJson, getSelfUid, resolveUid } from './utils.js';
3
3
  cli({
4
4
  site: 'bilibili',
5
5
  name: 'following',
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { apiGet, payloadData } from '../../bilibili.js';
2
+ import { apiGet, payloadData } from './utils.js';
3
3
  cli({
4
4
  site: 'bilibili',
5
5
  name: 'history',