@jackwener/opencli 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/actions/setup-chrome/action.yml +5 -4
- package/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +37 -3
- package/.github/workflows/e2e-headed.yml +16 -3
- package/CHANGELOG.md +23 -0
- package/PRIVACY.md +57 -0
- package/README.md +36 -7
- package/README.zh-CN.md +13 -6
- package/SKILL.md +103 -2
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1838 -151
- package/dist/cli.js +34 -3
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/apple-podcasts/search.js +2 -1
- package/dist/clis/arxiv/search.js +2 -2
- package/dist/clis/bbc/news.js +0 -1
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/ctrip/search.js +0 -1
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
- package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
- package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.js +5 -0
- package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
- package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
- package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
- package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
- package/dist/clis/douyin/_shared/sts2.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
- package/dist/clis/douyin/_shared/text-extra.js +15 -0
- package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
- package/dist/clis/douyin/_shared/timing.d.ts +2 -0
- package/dist/clis/douyin/_shared/timing.js +22 -0
- package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/timing.test.js +28 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
- package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
- package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
- package/dist/clis/douyin/_shared/tos-upload.js +295 -0
- package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
- package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
- package/dist/clis/douyin/_shared/transcode.js +45 -0
- package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/transcode.test.js +93 -0
- package/dist/clis/douyin/_shared/types.d.ts +26 -0
- package/dist/clis/douyin/_shared/types.js +1 -0
- package/dist/clis/douyin/activities.d.ts +1 -0
- package/dist/clis/douyin/activities.js +20 -0
- package/dist/clis/douyin/activities.test.d.ts +1 -0
- package/dist/clis/douyin/activities.test.js +22 -0
- package/dist/clis/douyin/collections.d.ts +1 -0
- package/dist/clis/douyin/collections.js +22 -0
- package/dist/clis/douyin/collections.test.d.ts +1 -0
- package/dist/clis/douyin/collections.test.js +23 -0
- package/dist/clis/douyin/delete.d.ts +1 -0
- package/dist/clis/douyin/delete.js +18 -0
- package/dist/clis/douyin/delete.test.d.ts +1 -0
- package/dist/clis/douyin/delete.test.js +11 -0
- package/dist/clis/douyin/draft.d.ts +14 -0
- package/dist/clis/douyin/draft.js +237 -0
- package/dist/clis/douyin/draft.test.d.ts +1 -0
- package/dist/clis/douyin/draft.test.js +11 -0
- package/dist/clis/douyin/drafts.d.ts +1 -0
- package/dist/clis/douyin/drafts.js +23 -0
- package/dist/clis/douyin/drafts.test.d.ts +1 -0
- package/dist/clis/douyin/drafts.test.js +11 -0
- package/dist/clis/douyin/hashtag.d.ts +1 -0
- package/dist/clis/douyin/hashtag.js +45 -0
- package/dist/clis/douyin/hashtag.test.d.ts +1 -0
- package/dist/clis/douyin/hashtag.test.js +25 -0
- package/dist/clis/douyin/location.d.ts +1 -0
- package/dist/clis/douyin/location.js +24 -0
- package/dist/clis/douyin/location.test.d.ts +1 -0
- package/dist/clis/douyin/location.test.js +23 -0
- package/dist/clis/douyin/profile.d.ts +1 -0
- package/dist/clis/douyin/profile.js +28 -0
- package/dist/clis/douyin/profile.test.d.ts +1 -0
- package/dist/clis/douyin/profile.test.js +11 -0
- package/dist/clis/douyin/publish.d.ts +14 -0
- package/dist/clis/douyin/publish.js +288 -0
- package/dist/clis/douyin/publish.test.d.ts +1 -0
- package/dist/clis/douyin/publish.test.js +38 -0
- package/dist/clis/douyin/stats.d.ts +1 -0
- package/dist/clis/douyin/stats.js +27 -0
- package/dist/clis/douyin/stats.test.d.ts +1 -0
- package/dist/clis/douyin/stats.test.js +22 -0
- package/dist/clis/douyin/update.d.ts +1 -0
- package/dist/clis/douyin/update.js +31 -0
- package/dist/clis/douyin/update.test.d.ts +1 -0
- package/dist/clis/douyin/update.test.js +11 -0
- package/dist/clis/douyin/videos.d.ts +1 -0
- package/dist/clis/douyin/videos.js +34 -0
- package/dist/clis/douyin/videos.test.d.ts +1 -0
- package/dist/clis/douyin/videos.test.js +11 -0
- package/dist/clis/hackernews/search.yaml +1 -1
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/instagram/search.yaml +2 -1
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/search.yaml +3 -1
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/medium/search.js +1 -1
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/reuters/search.js +0 -1
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +7 -4
- package/dist/clis/twitter/search.test.js +56 -2
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/weibo/comments.d.ts +1 -0
- package/dist/clis/weibo/comments.js +53 -0
- package/dist/clis/weibo/feed.d.ts +1 -0
- package/dist/clis/weibo/feed.js +56 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weibo/me.d.ts +1 -0
- package/dist/clis/weibo/me.js +76 -0
- package/dist/clis/weibo/post.d.ts +1 -0
- package/dist/clis/weibo/post.js +75 -0
- package/dist/clis/weibo/user.d.ts +1 -0
- package/dist/clis/weibo/user.js +63 -0
- package/dist/clis/weibo/utils.d.ts +6 -0
- package/dist/clis/weibo/utils.js +30 -0
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/search.js +3 -2
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +114 -18
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +119 -0
- package/dist/clis/xueqiu/search.yaml +2 -1
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/channel.d.ts +1 -0
- package/dist/clis/youtube/channel.js +150 -0
- package/dist/clis/youtube/comments.d.ts +1 -0
- package/dist/clis/youtube/comments.js +95 -0
- package/dist/clis/youtube/search.js +0 -1
- package/dist/clis/zhihu/search.yaml +2 -1
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +8 -14
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +9 -2
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +15 -32
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +21 -1
- package/dist/external-clis.yaml +0 -17
- package/dist/hooks.js +2 -0
- package/dist/main.js +5 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin.d.ts +38 -5
- package/dist/plugin.js +267 -33
- package/dist/plugin.test.js +220 -3
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.js +1 -1
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +1 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/weread-search-regression.test.d.ts +1 -0
- package/dist/weread-search-regression.test.js +39 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +17 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/douyin.md +75 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/browser/twitter.md +6 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +12 -3
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +87 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/dist/background.js +508 -518
- package/extension/manifest.json +6 -2
- package/extension/package.json +2 -1
- package/extension/popup.html +84 -0
- package/extension/popup.js +25 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +22 -1
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +2 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +35 -3
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/apple-podcasts/search.ts +2 -1
- package/src/clis/arxiv/search.ts +2 -2
- package/src/clis/bbc/news.ts +0 -1
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/ctrip/search.ts +0 -1
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
- package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
- package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
- package/src/clis/douyin/_shared/creation-id.ts +8 -0
- package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
- package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
- package/src/clis/douyin/_shared/sts2.ts +20 -0
- package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
- package/src/clis/douyin/_shared/text-extra.ts +33 -0
- package/src/clis/douyin/_shared/timing.test.ts +38 -0
- package/src/clis/douyin/_shared/timing.ts +22 -0
- package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
- package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
- package/src/clis/douyin/_shared/tos-upload.ts +444 -0
- package/src/clis/douyin/_shared/transcode.test.ts +117 -0
- package/src/clis/douyin/_shared/transcode.ts +78 -0
- package/src/clis/douyin/_shared/types.ts +29 -0
- package/src/clis/douyin/activities.test.ts +25 -0
- package/src/clis/douyin/activities.ts +23 -0
- package/src/clis/douyin/collections.test.ts +26 -0
- package/src/clis/douyin/collections.ts +25 -0
- package/src/clis/douyin/delete.test.ts +12 -0
- package/src/clis/douyin/delete.ts +20 -0
- package/src/clis/douyin/draft.test.ts +12 -0
- package/src/clis/douyin/draft.ts +282 -0
- package/src/clis/douyin/drafts.test.ts +12 -0
- package/src/clis/douyin/drafts.ts +27 -0
- package/src/clis/douyin/hashtag.test.ts +28 -0
- package/src/clis/douyin/hashtag.ts +56 -0
- package/src/clis/douyin/location.test.ts +26 -0
- package/src/clis/douyin/location.ts +27 -0
- package/src/clis/douyin/profile.test.ts +12 -0
- package/src/clis/douyin/profile.ts +37 -0
- package/src/clis/douyin/publish.test.ts +45 -0
- package/src/clis/douyin/publish.ts +340 -0
- package/src/clis/douyin/stats.test.ts +25 -0
- package/src/clis/douyin/stats.ts +30 -0
- package/src/clis/douyin/update.test.ts +12 -0
- package/src/clis/douyin/update.ts +43 -0
- package/src/clis/douyin/videos.test.ts +12 -0
- package/src/clis/douyin/videos.ts +49 -0
- package/src/clis/hackernews/search.yaml +1 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/instagram/search.yaml +2 -1
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/search.yaml +3 -1
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/medium/search.ts +1 -1
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/reuters/search.ts +0 -1
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +71 -2
- package/src/clis/twitter/search.ts +8 -4
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/weibo/comments.ts +54 -0
- package/src/clis/weibo/feed.ts +57 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weibo/me.ts +77 -0
- package/src/clis/weibo/post.ts +77 -0
- package/src/clis/weibo/user.ts +64 -0
- package/src/clis/weibo/utils.ts +32 -0
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/search.ts +3 -2
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +137 -0
- package/src/clis/xiaohongshu/publish.ts +129 -18
- package/src/clis/xueqiu/search.yaml +2 -1
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/channel.ts +155 -0
- package/src/clis/youtube/comments.ts +97 -0
- package/src/clis/youtube/search.ts +0 -1
- package/src/clis/zhihu/search.yaml +2 -1
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +8 -15
- package/src/doctor.ts +13 -2
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/errors.ts +25 -66
- package/src/execution.ts +28 -3
- package/src/external-clis.yaml +0 -17
- package/src/hooks.ts +1 -0
- package/src/main.ts +6 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin.test.ts +246 -2
- package/src/plugin.ts +338 -36
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +1 -1
- package/src/serialization.ts +4 -0
- package/src/types.ts +1 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/weread-search-regression.test.ts +44 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +162 -0
- package/tests/e2e/browser-public.test.ts +55 -136
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +34 -17
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
package/dist/commanderAdapter.js
CHANGED
|
@@ -14,14 +14,30 @@ import { fullName, getRegistry } from './registry.js';
|
|
|
14
14
|
import { formatRegistryHelpText } from './serialization.js';
|
|
15
15
|
import { render as renderOutput } from './output.js';
|
|
16
16
|
import { executeCommand } from './execution.js';
|
|
17
|
-
import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
|
|
17
|
+
import { CliError, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
|
|
18
|
+
import { checkDaemonStatus } from './browser/discover.js';
|
|
19
|
+
export function normalizeArgValue(argType, value, name) {
|
|
20
|
+
if (argType !== 'bool')
|
|
21
|
+
return value;
|
|
22
|
+
if (typeof value === 'boolean')
|
|
23
|
+
return value;
|
|
24
|
+
if (value == null || value === '')
|
|
25
|
+
return false;
|
|
26
|
+
const normalized = String(value).trim().toLowerCase();
|
|
27
|
+
if (normalized === 'true')
|
|
28
|
+
return true;
|
|
29
|
+
if (normalized === 'false')
|
|
30
|
+
return false;
|
|
31
|
+
throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
|
|
32
|
+
}
|
|
18
33
|
/**
|
|
19
34
|
* Register a single CliCommand as a Commander subcommand.
|
|
20
35
|
*/
|
|
21
36
|
export function registerCommandToProgram(siteCmd, cmd) {
|
|
22
37
|
if (siteCmd.commands.some((c) => c.name() === cmd.name))
|
|
23
38
|
return;
|
|
24
|
-
const
|
|
39
|
+
const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
|
|
40
|
+
const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
|
|
25
41
|
// Register positional args first, then named options
|
|
26
42
|
const positionalArgs = [];
|
|
27
43
|
for (const arg of cmd.args) {
|
|
@@ -48,27 +64,32 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
48
64
|
const actionOpts = actionArgs[positionalArgs.length] ?? {};
|
|
49
65
|
const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts : {};
|
|
50
66
|
const startTime = Date.now();
|
|
51
|
-
// ── Collect kwargs ──────────────────────────────────────────────────
|
|
52
|
-
const kwargs = {};
|
|
53
|
-
for (let i = 0; i < positionalArgs.length; i++) {
|
|
54
|
-
const v = actionArgs[i];
|
|
55
|
-
if (v !== undefined)
|
|
56
|
-
kwargs[positionalArgs[i].name] = v;
|
|
57
|
-
}
|
|
58
|
-
for (const arg of cmd.args) {
|
|
59
|
-
if (arg.positional)
|
|
60
|
-
continue;
|
|
61
|
-
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
|
|
62
|
-
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
63
|
-
if (v !== undefined)
|
|
64
|
-
kwargs[arg.name] = v;
|
|
65
|
-
}
|
|
66
67
|
// ── Execute + render ────────────────────────────────────────────────
|
|
67
68
|
try {
|
|
69
|
+
// ── Collect kwargs ────────────────────────────────────────────────
|
|
70
|
+
const kwargs = {};
|
|
71
|
+
for (let i = 0; i < positionalArgs.length; i++) {
|
|
72
|
+
const v = actionArgs[i];
|
|
73
|
+
if (v !== undefined)
|
|
74
|
+
kwargs[positionalArgs[i].name] = v;
|
|
75
|
+
}
|
|
76
|
+
for (const arg of cmd.args) {
|
|
77
|
+
if (arg.positional)
|
|
78
|
+
continue;
|
|
79
|
+
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
|
|
80
|
+
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
81
|
+
if (v !== undefined)
|
|
82
|
+
kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
|
|
83
|
+
}
|
|
68
84
|
const verbose = optionsRecord.verbose === true;
|
|
69
85
|
const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
70
86
|
if (verbose)
|
|
71
87
|
process.env.OPENCLI_VERBOSE = '1';
|
|
88
|
+
if (cmd.deprecated) {
|
|
89
|
+
const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
|
|
90
|
+
const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
|
|
91
|
+
console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
|
|
92
|
+
}
|
|
72
93
|
const result = await executeCommand(cmd, kwargs, verbose);
|
|
73
94
|
if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
74
95
|
console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
|
|
@@ -84,22 +105,148 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
84
105
|
});
|
|
85
106
|
}
|
|
86
107
|
catch (err) {
|
|
87
|
-
|
|
88
|
-
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
89
|
-
console.error(chalk.red(`${icon} ${err.message}`));
|
|
90
|
-
if (err.hint)
|
|
91
|
-
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
92
|
-
}
|
|
93
|
-
else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
|
|
94
|
-
console.error(chalk.red(err.stack));
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
98
|
-
}
|
|
108
|
+
await renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
99
109
|
process.exitCode = 1;
|
|
100
110
|
}
|
|
101
111
|
});
|
|
102
112
|
}
|
|
113
|
+
// ── Error rendering ──────────────────────────────────────────────────────────
|
|
114
|
+
const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
|
|
115
|
+
/** Pattern-based classifier for untyped errors thrown by adapters. */
|
|
116
|
+
function classifyGenericError(msg) {
|
|
117
|
+
const m = msg.toLowerCase();
|
|
118
|
+
if (/not logged in|login required|please log in|未登录|请先登录|authentication required|cookie expired/.test(m))
|
|
119
|
+
return 'auth';
|
|
120
|
+
// Match "HTTP 404", "status: 500", "status 403", bare "404 Not Found", etc.
|
|
121
|
+
if (/\b(status[: ]+)?[45]\d{2}\b|http[/ ][45]\d{2}/.test(m))
|
|
122
|
+
return 'http';
|
|
123
|
+
if (/not found|未找到|could not find|no .+ found/.test(m))
|
|
124
|
+
return 'not-found';
|
|
125
|
+
return 'other';
|
|
126
|
+
}
|
|
127
|
+
/** Render a status line for BrowserConnectError based on real-time or kind-derived state. */
|
|
128
|
+
function renderBridgeStatus(running, extensionConnected) {
|
|
129
|
+
const ok = chalk.green('✓');
|
|
130
|
+
const fail = chalk.red('✗');
|
|
131
|
+
console.error(` Daemon ${running ? ok : fail} ${running ? 'running' : 'not running'}`);
|
|
132
|
+
console.error(` Extension ${extensionConnected ? ok : fail} ${extensionConnected ? 'connected' : 'not connected'}`);
|
|
133
|
+
console.error();
|
|
134
|
+
if (!running) {
|
|
135
|
+
console.error(chalk.yellow(' Run the command again — daemon should auto-start.'));
|
|
136
|
+
console.error(chalk.dim(' Still failing? Run: opencli doctor'));
|
|
137
|
+
}
|
|
138
|
+
else if (!extensionConnected) {
|
|
139
|
+
console.error(chalk.yellow(' Install the Browser Bridge extension to continue:'));
|
|
140
|
+
console.error(chalk.dim(' 1. Download from github.com/jackwener/opencli/releases'));
|
|
141
|
+
console.error(chalk.dim(' 2. chrome://extensions → Enable Developer Mode → Load unpacked'));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.error(chalk.yellow(' Connection failed despite extension being active.'));
|
|
145
|
+
console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function renderError(err, cmdName, verbose) {
|
|
149
|
+
// ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
|
|
150
|
+
if (err instanceof BrowserConnectError) {
|
|
151
|
+
console.error(chalk.red('🔌 Browser Bridge not connected'));
|
|
152
|
+
console.error();
|
|
153
|
+
try {
|
|
154
|
+
// 300ms matches execution.ts — localhost responds in <50ms when running.
|
|
155
|
+
const status = await checkDaemonStatus({ timeout: 300 });
|
|
156
|
+
renderBridgeStatus(status.running, status.extensionConnected);
|
|
157
|
+
}
|
|
158
|
+
catch (_statusErr) {
|
|
159
|
+
// checkDaemonStatus itself failed — derive best-guess state from kind.
|
|
160
|
+
const running = err.kind !== 'daemon-not-running';
|
|
161
|
+
const extensionConnected = err.kind === 'command-failed';
|
|
162
|
+
renderBridgeStatus(running, extensionConnected);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// ── AuthRequiredError ─────────────────────────────────────────────────
|
|
167
|
+
if (err instanceof AuthRequiredError) {
|
|
168
|
+
console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
|
|
169
|
+
// Respect custom hints set by the adapter; fall back to generic guidance.
|
|
170
|
+
console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// ── TimeoutError ──────────────────────────────────────────────────────
|
|
174
|
+
if (err instanceof TimeoutError) {
|
|
175
|
+
console.error(chalk.red(`⏱ ${err.message}`));
|
|
176
|
+
console.error(chalk.yellow('→ Try again, or raise the limit:'));
|
|
177
|
+
console.error(chalk.dim(` OPENCLI_BROWSER_COMMAND_TIMEOUT=60 ${cmdName}`));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// ── SelectorError / EmptyResultError: likely outdated adapter ─────────
|
|
181
|
+
if (err instanceof SelectorError || err instanceof EmptyResultError) {
|
|
182
|
+
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
183
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
184
|
+
console.error(chalk.yellow('→ The page structure may have changed — this adapter may be outdated.'));
|
|
185
|
+
console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
|
|
186
|
+
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// ── ArgumentError ─────────────────────────────────────────────────────
|
|
190
|
+
if (err instanceof ArgumentError) {
|
|
191
|
+
console.error(chalk.red(`❌ ${err.message}`));
|
|
192
|
+
if (err.hint)
|
|
193
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// ── AdapterLoadError ──────────────────────────────────────────────────
|
|
197
|
+
if (err instanceof AdapterLoadError) {
|
|
198
|
+
console.error(chalk.red(`📦 ${err.message}`));
|
|
199
|
+
if (err.hint)
|
|
200
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// ── CommandExecutionError ─────────────────────────────────────────────
|
|
204
|
+
if (err instanceof CommandExecutionError) {
|
|
205
|
+
console.error(chalk.red(`💥 ${err.message}`));
|
|
206
|
+
if (err.hint) {
|
|
207
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.error(chalk.dim(` Add --verbose for details, or report: ${ISSUES_URL}`));
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// ── Other typed CliError (fallback for future codes) ──────────────────
|
|
215
|
+
if (err instanceof CliError) {
|
|
216
|
+
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
217
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
218
|
+
if (err.hint)
|
|
219
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// ── Generic Error from adapters: classify by message pattern ──────────
|
|
223
|
+
const msg = getErrorMessage(err);
|
|
224
|
+
const kind = classifyGenericError(msg);
|
|
225
|
+
if (kind === 'auth') {
|
|
226
|
+
console.error(chalk.red(`🔒 ${msg}`));
|
|
227
|
+
console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (kind === 'http') {
|
|
231
|
+
console.error(chalk.red(`🌐 ${msg}`));
|
|
232
|
+
console.error(chalk.yellow('→ Check your login status, or the site may be temporarily unavailable.'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (kind === 'not-found') {
|
|
236
|
+
console.error(chalk.red(`📭 ${msg}`));
|
|
237
|
+
console.error(chalk.yellow('→ The resource was not found. The adapter or page structure may have changed.'));
|
|
238
|
+
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// ── Unknown error: show stack in verbose mode ─────────────────────────
|
|
242
|
+
if (verbose && err instanceof Error && err.stack) {
|
|
243
|
+
console.error(chalk.red(err.stack));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
console.error(chalk.red(`💥 Unexpected error: ${msg}`));
|
|
247
|
+
console.error(chalk.dim(` Run with --verbose for details, or report: ${ISSUES_URL}`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
103
250
|
/**
|
|
104
251
|
* Register all commands from the registry onto a Commander program.
|
|
105
252
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
|
|
4
|
+
mockExecuteCommand: vi.fn(),
|
|
5
|
+
mockRenderOutput: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('./execution.js', () => ({
|
|
8
|
+
executeCommand: mockExecuteCommand,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('./output.js', () => ({
|
|
11
|
+
render: mockRenderOutput,
|
|
12
|
+
}));
|
|
13
|
+
import { registerCommandToProgram } from './commanderAdapter.js';
|
|
14
|
+
describe('commanderAdapter arg passing', () => {
|
|
15
|
+
const cmd = {
|
|
16
|
+
site: 'paperreview',
|
|
17
|
+
name: 'submit',
|
|
18
|
+
description: 'Submit a PDF',
|
|
19
|
+
browser: false,
|
|
20
|
+
args: [
|
|
21
|
+
{ name: 'pdf', positional: true, required: true, help: 'Path to the paper PDF' },
|
|
22
|
+
{ name: 'dry-run', type: 'bool', default: false, help: 'Validate only' },
|
|
23
|
+
{ name: 'prepare-only', type: 'bool', default: false, help: 'Prepare only' },
|
|
24
|
+
],
|
|
25
|
+
func: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
mockExecuteCommand.mockReset();
|
|
29
|
+
mockExecuteCommand.mockResolvedValue([]);
|
|
30
|
+
mockRenderOutput.mockReset();
|
|
31
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
32
|
+
process.exitCode = undefined;
|
|
33
|
+
});
|
|
34
|
+
it('passes bool flag values through to executeCommand for coercion', async () => {
|
|
35
|
+
const program = new Command();
|
|
36
|
+
const siteCmd = program.command('paperreview');
|
|
37
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
38
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'false']);
|
|
39
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
40
|
+
const kwargs = mockExecuteCommand.mock.calls[0][1];
|
|
41
|
+
expect(kwargs.pdf).toBe('./paper.pdf');
|
|
42
|
+
expect(kwargs).toHaveProperty('dry-run');
|
|
43
|
+
});
|
|
44
|
+
it('passes valueless bool flags as true to executeCommand', async () => {
|
|
45
|
+
const program = new Command();
|
|
46
|
+
const siteCmd = program.command('paperreview');
|
|
47
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
48
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--prepare-only']);
|
|
49
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
50
|
+
const kwargs = mockExecuteCommand.mock.calls[0][1];
|
|
51
|
+
expect(kwargs.pdf).toBe('./paper.pdf');
|
|
52
|
+
expect(kwargs['prepare-only']).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('rejects invalid bool values before calling executeCommand', async () => {
|
|
55
|
+
const program = new Command();
|
|
56
|
+
const siteCmd = program.command('paperreview');
|
|
57
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
58
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
|
|
59
|
+
// normalizeArgValue validates bools eagerly; executeCommand should not be reached
|
|
60
|
+
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|
package/dist/daemon.js
CHANGED
|
@@ -25,6 +25,7 @@ const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_P
|
|
|
25
25
|
const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
26
26
|
// ─── State ───────────────────────────────────────────────────────────
|
|
27
27
|
let extensionWs = null;
|
|
28
|
+
let extensionVersion = null;
|
|
28
29
|
const pending = new Map();
|
|
29
30
|
let idleTimer = null;
|
|
30
31
|
const LOG_BUFFER_SIZE = 200;
|
|
@@ -103,6 +104,7 @@ async function handleRequest(req, res) {
|
|
|
103
104
|
jsonResponse(res, 200, {
|
|
104
105
|
ok: true,
|
|
105
106
|
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
107
|
+
extensionVersion,
|
|
106
108
|
pending: pending.size,
|
|
107
109
|
});
|
|
108
110
|
return;
|
|
@@ -195,6 +197,11 @@ wss.on('connection', (ws) => {
|
|
|
195
197
|
ws.on('message', (data) => {
|
|
196
198
|
try {
|
|
197
199
|
const msg = JSON.parse(data.toString());
|
|
200
|
+
// Handle hello message from extension (version handshake)
|
|
201
|
+
if (msg.type === 'hello') {
|
|
202
|
+
extensionVersion = typeof msg.version === 'string' ? msg.version : null;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
198
205
|
// Handle log messages from extension
|
|
199
206
|
if (msg.type === 'log') {
|
|
200
207
|
const prefix = msg.level === 'error' ? '❌' : msg.level === 'warn' ? '⚠️' : '📋';
|
|
@@ -219,6 +226,7 @@ wss.on('connection', (ws) => {
|
|
|
219
226
|
clearInterval(heartbeatInterval);
|
|
220
227
|
if (extensionWs === ws) {
|
|
221
228
|
extensionWs = null;
|
|
229
|
+
extensionVersion = null;
|
|
222
230
|
// Reject all pending requests since the extension is gone
|
|
223
231
|
for (const [id, p] of pending) {
|
|
224
232
|
clearTimeout(p.timer);
|
|
@@ -229,8 +237,16 @@ wss.on('connection', (ws) => {
|
|
|
229
237
|
});
|
|
230
238
|
ws.on('error', () => {
|
|
231
239
|
clearInterval(heartbeatInterval);
|
|
232
|
-
if (extensionWs === ws)
|
|
240
|
+
if (extensionWs === ws) {
|
|
233
241
|
extensionWs = null;
|
|
242
|
+
extensionVersion = null;
|
|
243
|
+
// Reject pending requests in case 'close' does not follow this 'error'
|
|
244
|
+
for (const [, p] of pending) {
|
|
245
|
+
clearTimeout(p.timer);
|
|
246
|
+
p.reject(new Error('Extension disconnected'));
|
|
247
|
+
}
|
|
248
|
+
pending.clear();
|
|
249
|
+
}
|
|
234
250
|
});
|
|
235
251
|
});
|
|
236
252
|
// ─── Start ───────────────────────────────────────────────────────────
|
package/dist/discovery.js
CHANGED
|
@@ -19,6 +19,7 @@ import { log } from './logger.js';
|
|
|
19
19
|
export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
|
|
20
20
|
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
21
21
|
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
22
|
+
import { parseYamlArgs } from './yaml-schema.js';
|
|
22
23
|
function parseStrategy(rawStrategy, fallback = Strategy.COOKIE) {
|
|
23
24
|
if (!rawStrategy)
|
|
24
25
|
return fallback;
|
|
@@ -71,6 +72,8 @@ async function loadFromManifest(manifestPath, clisDir) {
|
|
|
71
72
|
pipeline: entry.pipeline,
|
|
72
73
|
timeoutSeconds: entry.timeout,
|
|
73
74
|
source: `manifest:${entry.site}/${entry.name}`,
|
|
75
|
+
deprecated: entry.deprecated,
|
|
76
|
+
replacedBy: entry.replacedBy,
|
|
74
77
|
navigateBefore: entry.navigateBefore,
|
|
75
78
|
};
|
|
76
79
|
registerCommand(cmd);
|
|
@@ -91,6 +94,8 @@ async function loadFromManifest(manifestPath, clisDir) {
|
|
|
91
94
|
columns: entry.columns,
|
|
92
95
|
timeoutSeconds: entry.timeout,
|
|
93
96
|
source: modulePath,
|
|
97
|
+
deprecated: entry.deprecated,
|
|
98
|
+
replacedBy: entry.replacedBy,
|
|
94
99
|
navigateBefore: entry.navigateBefore,
|
|
95
100
|
_lazy: true,
|
|
96
101
|
_modulePath: modulePath,
|
|
@@ -153,20 +158,7 @@ async function registerYamlCli(filePath, defaultSite) {
|
|
|
153
158
|
const strategyStr = cliDef.strategy ?? (cliDef.browser === false ? 'public' : 'cookie');
|
|
154
159
|
const strategy = parseStrategy(strategyStr);
|
|
155
160
|
const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
|
|
156
|
-
const args =
|
|
157
|
-
if (cliDef.args && typeof cliDef.args === 'object') {
|
|
158
|
-
for (const [argName, argDef] of Object.entries(cliDef.args)) {
|
|
159
|
-
args.push({
|
|
160
|
-
name: argName,
|
|
161
|
-
type: argDef?.type ?? 'str',
|
|
162
|
-
default: argDef?.default,
|
|
163
|
-
required: argDef?.required ?? false,
|
|
164
|
-
positional: argDef?.positional ?? false,
|
|
165
|
-
help: argDef?.description ?? argDef?.help ?? '',
|
|
166
|
-
choices: argDef?.choices,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
161
|
+
const args = parseYamlArgs(cliDef.args);
|
|
170
162
|
const cmd = {
|
|
171
163
|
site,
|
|
172
164
|
name,
|
|
@@ -179,6 +171,8 @@ async function registerYamlCli(filePath, defaultSite) {
|
|
|
179
171
|
pipeline: cliDef.pipeline,
|
|
180
172
|
timeoutSeconds: cliDef.timeout,
|
|
181
173
|
source: filePath,
|
|
174
|
+
deprecated: cliDef.deprecated,
|
|
175
|
+
replacedBy: cliDef.replacedBy,
|
|
182
176
|
navigateBefore: cliDef.navigateBefore,
|
|
183
177
|
};
|
|
184
178
|
registerCommand(cmd);
|
package/dist/doctor.d.ts
CHANGED
package/dist/doctor.js
CHANGED
|
@@ -10,6 +10,7 @@ import { checkDaemonStatus } from './browser/discover.js';
|
|
|
10
10
|
import { BrowserBridge } from './browser/index.js';
|
|
11
11
|
import { listSessions } from './browser/daemon-client.js';
|
|
12
12
|
import { getErrorMessage } from './errors.js';
|
|
13
|
+
import { getRuntimeLabel } from './runtime-detect.js';
|
|
13
14
|
/**
|
|
14
15
|
* Test connectivity by attempting a real browser command.
|
|
15
16
|
*/
|
|
@@ -64,23 +65,29 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
64
65
|
if (connectivity && !connectivity.ok) {
|
|
65
66
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
66
67
|
}
|
|
68
|
+
if (status.extensionVersion && opts.cliVersion && status.extensionVersion !== opts.cliVersion) {
|
|
69
|
+
issues.push(`Extension version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
|
|
70
|
+
' Download the latest extension from: https://github.com/jackwener/opencli/releases');
|
|
71
|
+
}
|
|
67
72
|
return {
|
|
68
73
|
cliVersion: opts.cliVersion,
|
|
69
74
|
daemonRunning: status.running,
|
|
70
75
|
extensionConnected: status.extensionConnected,
|
|
76
|
+
extensionVersion: status.extensionVersion,
|
|
71
77
|
connectivity,
|
|
72
78
|
sessions,
|
|
73
79
|
issues,
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
export function renderBrowserDoctorReport(report) {
|
|
77
|
-
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
|
83
|
+
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
|
|
78
84
|
// Daemon status
|
|
79
85
|
const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
|
|
80
86
|
lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running'}`);
|
|
81
87
|
// Extension status
|
|
82
88
|
const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
|
|
83
|
-
|
|
89
|
+
const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
|
|
90
|
+
lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
|
|
84
91
|
// Connectivity
|
|
85
92
|
if (report.connectivity) {
|
|
86
93
|
const connIcon = report.connectivity.ok ? chalk.green('[OK]') : chalk.red('[FAIL]');
|
package/dist/download/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import * as path from 'node:path';
|
|
|
7
7
|
import * as https from 'node:https';
|
|
8
8
|
import * as http from 'node:http';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
|
+
import { Transform } from 'node:stream';
|
|
11
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
12
|
import { URL } from 'node:url';
|
|
11
13
|
import { isBinaryInstalled } from '../external.js';
|
|
12
14
|
import { getErrorMessage } from '../errors.js';
|
|
@@ -68,65 +70,75 @@ export async function httpDownload(url, destPath, options = {}, redirectCount =
|
|
|
68
70
|
if (cookies) {
|
|
69
71
|
requestHeaders['Cookie'] = cookies;
|
|
70
72
|
}
|
|
71
|
-
// Ensure directory exists
|
|
72
|
-
const dir = path.dirname(destPath);
|
|
73
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
74
73
|
const tempPath = `${destPath}.tmp`;
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
79
|
-
file.close();
|
|
80
|
-
if (fs.existsSync(tempPath))
|
|
81
|
-
fs.unlinkSync(tempPath);
|
|
82
|
-
if (redirectCount >= maxRedirects) {
|
|
83
|
-
resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
87
|
-
const originalHost = new URL(url).hostname;
|
|
88
|
-
const redirectHost = new URL(redirectUrl).hostname;
|
|
89
|
-
// Do not forward cookies when a redirect crosses host boundaries.
|
|
90
|
-
const redirectOptions = originalHost === redirectHost
|
|
91
|
-
? options
|
|
92
|
-
: { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
|
|
93
|
-
httpDownload(redirectUrl, destPath, redirectOptions, redirectCount + 1).then(resolve);
|
|
74
|
+
let settled = false;
|
|
75
|
+
const finish = (result) => {
|
|
76
|
+
if (settled)
|
|
94
77
|
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
resolve(result);
|
|
80
|
+
};
|
|
81
|
+
const cleanupTempFile = async () => {
|
|
82
|
+
try {
|
|
83
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
95
84
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (fs.existsSync(tempPath))
|
|
99
|
-
fs.unlinkSync(tempPath);
|
|
100
|
-
resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
101
|
-
return;
|
|
85
|
+
catch {
|
|
86
|
+
// Ignore cleanup errors so the original failure is preserved.
|
|
102
87
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
88
|
+
};
|
|
89
|
+
const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
|
|
90
|
+
void (async () => {
|
|
91
|
+
// Handle redirects before creating any file handles.
|
|
92
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
93
|
+
response.resume();
|
|
94
|
+
if (redirectCount >= maxRedirects) {
|
|
95
|
+
finish({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
99
|
+
const originalHost = new URL(url).hostname;
|
|
100
|
+
const redirectHost = new URL(redirectUrl).hostname;
|
|
101
|
+
const redirectOptions = originalHost === redirectHost
|
|
102
|
+
? options
|
|
103
|
+
: { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
|
|
104
|
+
finish(await httpDownload(redirectUrl, destPath, redirectOptions, redirectCount + 1));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (response.statusCode !== 200) {
|
|
108
|
+
response.resume();
|
|
109
|
+
finish({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
|
113
|
+
let received = 0;
|
|
114
|
+
const progressStream = new Transform({
|
|
115
|
+
transform(chunk, _encoding, callback) {
|
|
116
|
+
received += chunk.length;
|
|
117
|
+
if (onProgress)
|
|
118
|
+
onProgress(received, totalSize);
|
|
119
|
+
callback(null, chunk);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
124
|
+
await pipeline(response, progressStream, fs.createWriteStream(tempPath));
|
|
125
|
+
await fs.promises.rename(tempPath, destPath);
|
|
126
|
+
finish({ success: true, size: received });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
await cleanupTempFile();
|
|
130
|
+
finish({ success: false, size: 0, error: getErrorMessage(err) });
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
117
133
|
});
|
|
118
134
|
request.on('error', (err) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
void (async () => {
|
|
136
|
+
await cleanupTempFile();
|
|
137
|
+
finish({ success: false, size: 0, error: err.message });
|
|
138
|
+
})();
|
|
123
139
|
});
|
|
124
140
|
request.on('timeout', () => {
|
|
125
|
-
request.destroy();
|
|
126
|
-
file.close();
|
|
127
|
-
if (fs.existsSync(tempPath))
|
|
128
|
-
fs.unlinkSync(tempPath);
|
|
129
|
-
resolve({ success: false, size: 0, error: 'Timeout' });
|
|
141
|
+
request.destroy(new Error('Timeout'));
|
|
130
142
|
});
|
|
131
143
|
});
|
|
132
144
|
}
|
|
@@ -5,11 +5,19 @@ import * as path from 'node:path';
|
|
|
5
5
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
6
|
import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
|
|
7
7
|
const servers = [];
|
|
8
|
+
const tempDirs = [];
|
|
8
9
|
afterEach(async () => {
|
|
9
10
|
await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
|
|
10
11
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
11
12
|
})));
|
|
12
13
|
servers.length = 0;
|
|
14
|
+
for (const dir of tempDirs) {
|
|
15
|
+
try {
|
|
16
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
catch { /* ignore */ }
|
|
19
|
+
}
|
|
20
|
+
tempDirs.length = 0;
|
|
13
21
|
});
|
|
14
22
|
async function startServer(handler, hostname = '127.0.0.1') {
|
|
15
23
|
const server = http.createServer(handler);
|
|
@@ -21,7 +29,9 @@ async function startServer(handler, hostname = '127.0.0.1') {
|
|
|
21
29
|
}
|
|
22
30
|
return `http://${hostname}:${address.port}`;
|
|
23
31
|
}
|
|
24
|
-
|
|
32
|
+
// Windows Defender can briefly lock newly-written .tmp files, causing EPERM.
|
|
33
|
+
// Retry once to handle this flakiness.
|
|
34
|
+
describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
|
|
25
35
|
it('resolves relative redirects against the original URL', () => {
|
|
26
36
|
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
27
37
|
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
@@ -38,7 +48,8 @@ describe('download helpers', () => {
|
|
|
38
48
|
res.setHeader('Location', '/loop');
|
|
39
49
|
res.end();
|
|
40
50
|
});
|
|
41
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
51
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
52
|
+
tempDirs.push(tempDir);
|
|
42
53
|
const destPath = path.join(tempDir, 'file.txt');
|
|
43
54
|
const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
|
|
44
55
|
expect(result).toEqual({
|
|
@@ -60,7 +71,8 @@ describe('download helpers', () => {
|
|
|
60
71
|
res.setHeader('Location', targetUrl);
|
|
61
72
|
res.end();
|
|
62
73
|
});
|
|
63
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
74
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
75
|
+
tempDirs.push(tempDir);
|
|
64
76
|
const destPath = path.join(tempDir, 'redirect.txt');
|
|
65
77
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
|
|
66
78
|
expect(result).toEqual({ success: true, size: 2 });
|
|
@@ -79,7 +91,8 @@ describe('download helpers', () => {
|
|
|
79
91
|
res.setHeader('Location', targetUrl);
|
|
80
92
|
res.end();
|
|
81
93
|
});
|
|
82
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
94
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
95
|
+
tempDirs.push(tempDir);
|
|
83
96
|
const destPath = path.join(tempDir, 'redirect-header.txt');
|
|
84
97
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, {
|
|
85
98
|
headers: { Cookie: 'sid=header-cookie' },
|