@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/src/cli.ts CHANGED
@@ -1,57 +1,91 @@
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
+ */
7
+
1
8
  import { Command } from 'commander';
2
9
  import chalk from 'chalk';
3
- import { executeCommand } from './engine.js';
4
- import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
10
+ import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
11
+ import { serializeCommand, formatArgSummary } from './serialization.js';
5
12
  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';
13
+ import { getBrowserFactory, browserSession } from './runtime.js';
8
14
  import { PKG_VERSION } from './version.js';
9
15
  import { printCompletionScript } from './completion.js';
10
- import { CliError } from './errors.js';
11
- import { shouldUseBrowserSession } from './capabilityRouting.js';
12
16
  import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
17
+ import { registerAllCommands } from './commanderAdapter.js';
13
18
 
14
19
  export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
15
20
  const program = new Command();
16
- program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
21
+ // enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
22
+ // prerequisite for passThroughOptions to forward --help/--version to external binaries
23
+ program
24
+ .name('opencli')
25
+ .description('Make any website your CLI. Zero setup. AI-powered.')
26
+ .version(PKG_VERSION)
27
+ .enablePositionalOptions();
17
28
 
18
- // ── Built-in commands ──────────────────────────────────────────────────────
29
+ // ── Built-in: list ────────────────────────────────────────────────────────
19
30
 
20
- 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)')
31
+ program
32
+ .command('list')
33
+ .description('List all available CLI commands')
34
+ .option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
35
+ .option('--json', 'JSON output (deprecated)')
21
36
  .action((opts) => {
22
37
  const registry = getRegistry();
23
38
  const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b)));
24
- const rows = commands.map(c => ({
25
- command: fullName(c),
26
- site: c.site,
27
- name: c.name,
28
- description: c.description,
29
- strategy: strategyLabel(c),
30
- browser: c.browser,
31
- args: c.args.map(a => a.name).join(', '),
32
- }));
33
39
  const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format;
40
+ const isStructured = fmt === 'json' || fmt === 'yaml';
41
+
34
42
  if (fmt !== 'table') {
43
+ const rows = isStructured
44
+ ? commands.map(serializeCommand)
45
+ : commands.map(c => ({
46
+ command: fullName(c),
47
+ site: c.site,
48
+ name: c.name,
49
+ description: c.description,
50
+ strategy: strategyLabel(c),
51
+ browser: !!c.browser,
52
+ args: formatArgSummary(c.args),
53
+ }));
35
54
  renderOutput(rows, {
36
55
  fmt,
37
- columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'],
56
+ columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args',
57
+ ...(isStructured ? ['columns', 'domain'] : [])],
38
58
  title: 'opencli/list',
39
59
  source: 'opencli list',
40
60
  });
41
61
  return;
42
62
  }
63
+
64
+ // Table (default) — grouped by site
43
65
  const sites = new Map<string, CliCommand[]>();
44
- for (const cmd of commands) { const g = sites.get(cmd.site) ?? []; g.push(cmd); sites.set(cmd.site, g); }
45
- console.log(); console.log(chalk.bold(' opencli') + chalk.dim(' — available commands')); console.log();
66
+ for (const cmd of commands) {
67
+ const g = sites.get(cmd.site) ?? [];
68
+ g.push(cmd);
69
+ sites.set(cmd.site, g);
70
+ }
71
+
72
+ console.log();
73
+ console.log(chalk.bold(' opencli') + chalk.dim(' — available commands'));
74
+ console.log();
46
75
  for (const [site, cmds] of sites) {
47
76
  console.log(chalk.bold.cyan(` ${site}`));
48
- for (const cmd of cmds) { const tag = strategyLabel(cmd) === 'public' ? chalk.green('[public]') : chalk.yellow(`[${strategyLabel(cmd)}]`); console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`); }
77
+ for (const cmd of cmds) {
78
+ const tag = strategyLabel(cmd) === 'public'
79
+ ? chalk.green('[public]')
80
+ : chalk.yellow(`[${strategyLabel(cmd)}]`);
81
+ console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`);
82
+ }
49
83
  console.log();
50
84
  }
51
-
85
+
52
86
  const externalClis = loadExternalClis();
53
87
  if (externalClis.length > 0) {
54
- console.log(chalk.bold.cyan(` external CLIs`));
88
+ console.log(chalk.bold.cyan(' external CLIs'));
55
89
  for (const ext of externalClis) {
56
90
  const isInstalled = isBinaryInstalled(ext.binary);
57
91
  const tag = isInstalled ? chalk.green('[installed]') : chalk.yellow('[auto-install]');
@@ -59,17 +93,27 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
59
93
  }
60
94
  console.log();
61
95
  }
62
-
63
- console.log(chalk.dim(` ${commands.length} built-in commands across ${sites.size} sites, ${externalClis.length} external CLIs`)); console.log();
96
+
97
+ console.log(chalk.dim(` ${commands.length} built-in commands across ${sites.size} sites, ${externalClis.length} external CLIs`));
98
+ console.log();
64
99
  });
65
100
 
66
- program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name')
101
+ // ── Built-in: validate / verify ───────────────────────────────────────────
102
+
103
+ program
104
+ .command('validate')
105
+ .description('Validate CLI definitions')
106
+ .argument('[target]', 'site or site/name')
67
107
  .action(async (target) => {
68
108
  const { validateClisWithTarget, renderValidationReport } = await import('./validate.js');
69
109
  console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target)));
70
110
  });
71
111
 
72
- program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false)
112
+ program
113
+ .command('verify')
114
+ .description('Validate + smoke test')
115
+ .argument('[target]')
116
+ .option('--smoke', 'Run smoke tests', false)
73
117
  .action(async (target, opts) => {
74
118
  const { verifyClis, renderVerifyReport } = await import('./verify.js');
75
119
  const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
@@ -77,28 +121,91 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
77
121
  process.exitCode = r.ok ? 0 : 1;
78
122
  });
79
123
 
80
- 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,评论")')
81
- .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `explore:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}`; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory as any, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels, workspace }))); });
124
+ // ── Built-in: explore / synthesize / generate / cascade ───────────────────
82
125
 
83
- program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
84
- .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
126
+ program
127
+ .command('explore')
128
+ .alias('probe')
129
+ .description('Explore a website: discover APIs, stores, and recommend strategies')
130
+ .argument('<url>')
131
+ .option('--site <name>')
132
+ .option('--goal <text>')
133
+ .option('--wait <s>', '', '3')
134
+ .option('--auto', 'Enable interactive fuzzing')
135
+ .option('--click <labels>', 'Comma-separated labels to click before fuzzing')
136
+ .action(async (url, opts) => {
137
+ const { exploreUrl, renderExploreSummary } = await import('./explore.js');
138
+ const clickLabels = opts.click
139
+ ? opts.click.split(',').map((s: string) => s.trim())
140
+ : undefined;
141
+ const workspace = `explore:${inferHost(url, opts.site)}`;
142
+ const result = await exploreUrl(url, {
143
+ BrowserFactory: getBrowserFactory() as any,
144
+ site: opts.site,
145
+ goal: opts.goal,
146
+ waitSeconds: parseFloat(opts.wait),
147
+ auto: opts.auto,
148
+ clickLabels,
149
+ workspace,
150
+ });
151
+ console.log(renderExploreSummary(result));
152
+ });
85
153
 
86
- program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
87
- .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 { return new URL(url).host; } catch { return 'default'; } })()}`; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory as any, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site, workspace }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
154
+ program
155
+ .command('synthesize')
156
+ .description('Synthesize CLIs from explore')
157
+ .argument('<target>')
158
+ .option('--top <n>', '', '3')
159
+ .action(async (target, opts) => {
160
+ const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js');
161
+ console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) })));
162
+ });
163
+
164
+ program
165
+ .command('generate')
166
+ .description('One-shot: explore → synthesize → register')
167
+ .argument('<url>')
168
+ .option('--goal <text>')
169
+ .option('--site <name>')
170
+ .action(async (url, opts) => {
171
+ const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
172
+ const workspace = `generate:${inferHost(url, opts.site)}`;
173
+ const r = await generateCliFromUrl({
174
+ url,
175
+ BrowserFactory: getBrowserFactory() as any,
176
+ builtinClis: BUILTIN_CLIS,
177
+ userClis: USER_CLIS,
178
+ goal: opts.goal,
179
+ site: opts.site,
180
+ workspace,
181
+ });
182
+ console.log(renderGenerateSummary(r));
183
+ process.exitCode = r.ok ? 0 : 1;
184
+ });
88
185
 
89
- program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
186
+ program
187
+ .command('cascade')
188
+ .description('Strategy cascade: find simplest working strategy')
189
+ .argument('<url>')
190
+ .option('--site <name>')
90
191
  .action(async (url, opts) => {
91
192
  const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
92
- const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
93
- const result = await browserSession(BrowserFactory as any, async (page) => {
94
- // Navigate to the site first for cookie context
95
- try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {}
193
+ const workspace = `cascade:${inferHost(url, opts.site)}`;
194
+ const result = await browserSession(getBrowserFactory(), async (page) => {
195
+ try {
196
+ const siteUrl = new URL(url);
197
+ await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
198
+ await page.wait(2);
199
+ } catch {}
96
200
  return cascadeProbe(page, url);
97
- }, { workspace: `cascade:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}` });
201
+ }, { workspace });
98
202
  console.log(renderCascadeResult(result));
99
203
  });
100
204
 
101
- program.command('doctor')
205
+ // ── Built-in: doctor / setup / completion ─────────────────────────────────
206
+
207
+ program
208
+ .command('doctor')
102
209
  .description('Diagnose opencli browser bridge connectivity')
103
210
  .option('--live', 'Test browser connectivity (requires Chrome running)', false)
104
211
  .option('--sessions', 'Show active automation sessions', false)
@@ -108,66 +215,81 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
108
215
  console.log(renderBrowserDoctorReport(report));
109
216
  });
110
217
 
111
- program.command('setup')
218
+ program
219
+ .command('setup')
112
220
  .description('Interactive setup: verify browser bridge connectivity')
113
221
  .action(async () => {
114
222
  const { runSetup } = await import('./setup.js');
115
223
  await runSetup({ cliVersion: PKG_VERSION });
116
224
  });
117
225
 
118
- program.command('completion')
226
+ program
227
+ .command('completion')
119
228
  .description('Output shell completion script')
120
229
  .argument('<shell>', 'Shell type: bash, zsh, or fish')
121
230
  .action((shell) => {
122
231
  printCompletionScript(shell);
123
232
  });
124
233
 
234
+ // ── External CLIs ─────────────────────────────────────────────────────────
235
+
125
236
  const externalClis = loadExternalClis();
126
237
 
127
- program.command('install')
238
+ program
239
+ .command('install')
128
240
  .description('Install an external CLI')
129
241
  .argument('<name>', 'Name of the external CLI')
130
242
  .action((name: string) => {
131
243
  const ext = externalClis.find(e => e.name === name);
132
244
  if (!ext) {
133
- console.error(chalk.red(`External CLI '${name}' not found in registry.`));
134
- process.exitCode = 1;
135
- return;
245
+ console.error(chalk.red(`External CLI '${name}' not found in registry.`));
246
+ process.exitCode = 1;
247
+ return;
136
248
  }
137
249
  installExternalCli(ext);
138
250
  });
139
251
 
140
- program.command('register')
252
+ program
253
+ .command('register')
141
254
  .description('Register an external CLI')
142
255
  .argument('<name>', 'Name of the CLI')
143
256
  .option('--binary <bin>', 'Binary name if different from name')
144
257
  .option('--install <cmd>', 'Auto-install command')
145
258
  .option('--desc <text>', 'Description')
146
259
  .action((name, opts) => {
147
- registerExternalCli(name, opts.binary, opts.install, opts.desc);
260
+ registerExternalCli(name, { binary: opts.binary, install: opts.install, description: opts.desc });
148
261
  });
149
262
 
263
+ function passthroughExternal(name: string, parsedArgs?: string[]) {
264
+ const args = parsedArgs ?? (() => {
265
+ const idx = process.argv.indexOf(name);
266
+ return process.argv.slice(idx + 1);
267
+ })();
268
+ try {
269
+ executeExternalCli(name, args, externalClis);
270
+ } catch (err: any) {
271
+ console.error(chalk.red(`Error: ${err.message}`));
272
+ process.exitCode = 1;
273
+ }
274
+ }
275
+
150
276
  for (const ext of externalClis) {
151
277
  if (program.commands.some(c => c.name() === ext.name)) continue;
152
- program.command(ext.name)
278
+ program
279
+ .command(ext.name)
153
280
  .description(`(External) ${ext.description || ext.name}`)
281
+ .argument('[args...]')
154
282
  .allowUnknownOption()
155
- .action(() => {
156
- // Retrieve args passed to the external CLI
157
- // Commander consumes standard args before the action, so we must slice process.argv directly.
158
- const extIndex = process.argv.indexOf(ext.name);
159
- const args = process.argv.slice(extIndex + 1);
160
- executeExternalCli(ext.name, args).catch(err => {
161
- console.error(chalk.red(`Error: ${err.message}`));
162
- process.exitCode = 1;
163
- });
164
- });
283
+ .passThroughOptions()
284
+ .helpOption(false)
285
+ .action((args: string[]) => passthroughExternal(ext.name, args));
165
286
  }
166
287
 
167
- // ── Antigravity serve (built-in, long-running) ──────────────────────────────
288
+ // ── Antigravity serve (long-running, special case) ────────────────────────
168
289
 
169
290
  const antigravityCmd = program.command('antigravity').description('antigravity commands');
170
- antigravityCmd.command('serve')
291
+ antigravityCmd
292
+ .command('serve')
171
293
  .description('Start Anthropic-compatible API proxy for Antigravity')
172
294
  .option('--port <port>', 'Server port (default: 8082)', '8082')
173
295
  .action(async (opts) => {
@@ -175,88 +297,45 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
175
297
  await startServe({ port: parseInt(opts.port) });
176
298
  });
177
299
 
178
- // ── Dynamic site commands ──────────────────────────────────────────────────
300
+ // ── Dynamic adapter commands ──────────────────────────────────────────────
179
301
 
180
- const registry = getRegistry();
181
302
  const siteGroups = new Map<string, Command>();
182
- // Pre-seed with the antigravity command registered above to avoid duplicates
183
303
  siteGroups.set('antigravity', antigravityCmd);
304
+ registerAllCommands(program, siteGroups);
184
305
 
185
- for (const [, cmd] of registry) {
186
- let siteCmd = siteGroups.get(cmd.site);
187
- if (!siteCmd) { siteCmd = program.command(cmd.site).description(`${cmd.site} commands`); siteGroups.set(cmd.site, siteCmd); }
188
- // Skip if this subcommand was already hardcoded (e.g. antigravity serve)
189
- if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) continue;
190
- const subCmd = siteCmd.command(cmd.name).description(cmd.description);
191
-
192
- // Register positional args first, then named options
193
- const positionalArgs: typeof cmd.args = [];
194
- for (const arg of cmd.args) {
195
- if (arg.positional) {
196
- const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
197
- subCmd.argument(bracket, arg.help ?? '');
198
- positionalArgs.push(arg);
199
- } else {
200
- const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
201
- if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
202
- else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
203
- else subCmd.option(flag, arg.help ?? '');
204
- }
205
- }
206
- subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
207
-
208
- subCmd.action(async (...actionArgs: any[]) => {
209
- // Commander passes positional args first, then options object, then the Command
210
- const actionOpts = actionArgs[positionalArgs.length] ?? {};
211
- const startTime = Date.now();
212
- const kwargs: Record<string, any> = {};
213
-
214
- // Collect positional args
215
- for (let i = 0; i < positionalArgs.length; i++) {
216
- const arg = positionalArgs[i];
217
- const v = actionArgs[i];
218
- if (v !== undefined) kwargs[arg.name] = v;
219
- }
220
-
221
- // Collect named options
222
- for (const arg of cmd.args) {
223
- if (arg.positional) continue;
224
- const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
225
- const v = actionOpts[arg.name] ?? actionOpts[camelName];
226
- if (v !== undefined) kwargs[arg.name] = v;
227
- }
306
+ // ── Unknown command fallback ──────────────────────────────────────────────
228
307
 
229
- try {
230
- if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
231
- let result: any;
232
- if (shouldUseBrowserSession(cmd)) {
233
- const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
234
- result = await browserSession(BrowserFactory as any, async (page) => {
235
- // Cookie/header strategies require same-origin context for credentialed fetch.
236
- if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
237
- try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
238
- }
239
- return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
240
- }, { workspace: `site:${cmd.site}` });
241
- } else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
242
- if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
243
- 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.`));
244
- }
245
- const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
246
- 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) });
247
- } catch (err: any) {
248
- if (err instanceof CliError) {
249
- console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
250
- if (err.hint) console.error(chalk.yellow(`Hint: ${err.hint}`));
251
- } else if (actionOpts.verbose && err.stack) {
252
- console.error(chalk.red(err.stack));
253
- } else {
254
- console.error(chalk.red(`Error: ${err.message ?? err}`));
255
- }
256
- process.exitCode = 1;
257
- }
258
- });
259
- }
308
+ const DENY_LIST = new Set([
309
+ 'rm', 'sudo', 'dd', 'mkfs', 'fdisk', 'shutdown', 'reboot',
310
+ 'kill', 'killall', 'chmod', 'chown', 'passwd', 'su', 'mount',
311
+ 'umount', 'format', 'diskutil',
312
+ ]);
313
+
314
+ program.on('command:*', (operands: string[]) => {
315
+ const binary = operands[0];
316
+ if (DENY_LIST.has(binary)) {
317
+ console.error(chalk.red(`Refusing to register system command '${binary}'.`));
318
+ process.exitCode = 1;
319
+ return;
320
+ }
321
+ if (isBinaryInstalled(binary)) {
322
+ console.log(chalk.cyan(`🔹 Auto-discovered local CLI '${binary}'. Registering...`));
323
+ registerExternalCli(binary);
324
+ passthroughExternal(binary);
325
+ } else {
326
+ console.error(chalk.red(`error: unknown command '${binary}'`));
327
+ program.outputHelp();
328
+ process.exitCode = 1;
329
+ }
330
+ });
260
331
 
261
332
  program.parse();
262
333
  }
334
+
335
+ // ── Helpers ─────────────────────────────────────────────────────────────────
336
+
337
+ /** Infer a workspace-friendly hostname from a URL, with site override. */
338
+ function inferHost(url: string, site?: string): string {
339
+ if (site) return site;
340
+ try { return new URL(url).host; } catch { return 'default'; }
341
+ }
@@ -0,0 +1,95 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './search.js';
4
+ import './top.js';
5
+
6
+ describe('apple-podcasts search command', () => {
7
+ beforeEach(() => {
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ it('uses the positional query argument for the iTunes search request', async () => {
12
+ const cmd = getRegistry().get('apple-podcasts/search');
13
+ expect(cmd?.func).toBeTypeOf('function');
14
+
15
+ const fetchMock = vi.fn().mockResolvedValue({
16
+ ok: true,
17
+ json: () => Promise.resolve({
18
+ results: [
19
+ {
20
+ collectionId: 42,
21
+ collectionName: 'Machine Learning Guide',
22
+ artistName: 'OpenCLI',
23
+ trackCount: 12,
24
+ primaryGenreName: 'Technology',
25
+ },
26
+ ],
27
+ }),
28
+ });
29
+ vi.stubGlobal('fetch', fetchMock);
30
+
31
+ const result = await cmd!.func!(null as any, {
32
+ query: 'machine learning',
33
+ keyword: 'sports',
34
+ limit: 5,
35
+ });
36
+
37
+ expect(fetchMock).toHaveBeenCalledWith(
38
+ 'https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5',
39
+ );
40
+ expect(result).toEqual([
41
+ {
42
+ id: 42,
43
+ title: 'Machine Learning Guide',
44
+ author: 'OpenCLI',
45
+ episodes: 12,
46
+ genre: 'Technology',
47
+ },
48
+ ]);
49
+ });
50
+ });
51
+
52
+ describe('apple-podcasts top command', () => {
53
+ beforeEach(() => {
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ it('uses the canonical Apple charts host and maps ranked results', async () => {
58
+ const cmd = getRegistry().get('apple-podcasts/top');
59
+ expect(cmd?.func).toBeTypeOf('function');
60
+
61
+ const fetchMock = vi.fn().mockResolvedValue({
62
+ ok: true,
63
+ json: () => Promise.resolve({
64
+ feed: {
65
+ results: [
66
+ { id: '100', name: 'Top Show', artistName: 'Host A' },
67
+ { id: '101', name: 'Second Show', artistName: 'Host B' },
68
+ ],
69
+ },
70
+ }),
71
+ });
72
+ vi.stubGlobal('fetch', fetchMock);
73
+
74
+ const result = await cmd!.func!(null as any, { country: 'US', limit: 2 });
75
+
76
+ expect(fetchMock).toHaveBeenCalledWith(
77
+ 'https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json',
78
+ );
79
+ expect(result).toEqual([
80
+ { rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
81
+ { rank: 2, title: 'Second Show', author: 'Host B', id: '101' },
82
+ ]);
83
+ });
84
+
85
+ it('normalizes network failures into CliError output', async () => {
86
+ const cmd = getRegistry().get('apple-podcasts/top');
87
+ expect(cmd?.func).toBeTypeOf('function');
88
+
89
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
90
+
91
+ await expect(cmd!.func!(null as any, { country: 'us', limit: 3 })).rejects.toThrow(
92
+ 'Unable to reach Apple Podcasts charts for US',
93
+ );
94
+ });
95
+ });
@@ -9,12 +9,12 @@ cli({
9
9
  strategy: Strategy.PUBLIC,
10
10
  browser: false,
11
11
  args: [
12
- { name: 'keyword', positional: true, required: true, help: 'Search keyword' },
12
+ { name: 'query', positional: true, required: true, help: 'Search keyword' },
13
13
  { name: 'limit', type: 'int', default: 10, help: 'Max results' },
14
14
  ],
15
15
  columns: ['id', 'title', 'author', 'episodes', 'genre'],
16
16
  func: async (_page, args) => {
17
- const term = encodeURIComponent(args.keyword);
17
+ const term = encodeURIComponent(args.query);
18
18
  const limit = Math.max(1, Math.min(Number(args.limit), 25));
19
19
  const data = await itunesFetch(`/search?term=${term}&media=podcast&limit=${limit}`);
20
20
  if (!data.results?.length) throw new CliError('NOT_FOUND', 'No podcasts found', `Try a different keyword`);
@@ -2,7 +2,7 @@ import { cli, Strategy } from '../../registry.js';
2
2
  import { CliError } from '../../errors.js';
3
3
 
4
4
  // Apple Marketing Tools RSS API — public, no key required
5
- const CHARTS_URL = 'https://rss.applemarketingtools.com/api/v2';
5
+ const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
6
6
 
7
7
  cli({
8
8
  site: 'apple-podcasts',
@@ -19,7 +19,17 @@ cli({
19
19
  const limit = Math.max(1, Math.min(Number(args.limit), 100));
20
20
  const country = String(args.country || 'us').trim().toLowerCase();
21
21
  const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
22
- const resp = await fetch(url);
22
+ let resp: Response;
23
+ try {
24
+ resp = await fetch(url);
25
+ } catch (error: any) {
26
+ const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
27
+ throw new CliError(
28
+ 'FETCH_ERROR',
29
+ `Unable to reach Apple Podcasts charts for ${country.toUpperCase()}`,
30
+ `Apple charts may be temporarily unavailable (${reason}). Try again later.`,
31
+ );
32
+ }
23
33
  if (!resp.ok) throw new CliError('FETCH_ERROR', `Charts API HTTP ${resp.status}`, `Check country code: ${country}`);
24
34
  const data = await resp.json();
25
35
  const results = data?.feed?.results;
@@ -9,7 +9,7 @@ cli({
9
9
  strategy: Strategy.PUBLIC,
10
10
  browser: false,
11
11
  args: [
12
- { name: 'keyword', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
12
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
13
13
  { name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
14
14
  ],
15
15
  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
 
4
4
  cli({
5
5
  site: 'bilibili',
@@ -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
 
4
4
  cli({
5
5
  site: 'bilibili',
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import { apiGet, payloadData, getSelfUid, stripHtml } from '../../bilibili.js';
2
+ import { apiGet, payloadData, getSelfUid, stripHtml } from './utils.js';
3
3
 
4
4
  cli({
5
5
  site: 'bilibili',