@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/src/commanderAdapter.ts
CHANGED
|
@@ -16,7 +16,32 @@ import { type CliCommand, fullName, getRegistry } from './registry.js';
|
|
|
16
16
|
import { formatRegistryHelpText } from './serialization.js';
|
|
17
17
|
import { render as renderOutput } from './output.js';
|
|
18
18
|
import { executeCommand } from './execution.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
CliError,
|
|
21
|
+
ERROR_ICONS,
|
|
22
|
+
getErrorMessage,
|
|
23
|
+
BrowserConnectError,
|
|
24
|
+
AuthRequiredError,
|
|
25
|
+
TimeoutError,
|
|
26
|
+
SelectorError,
|
|
27
|
+
EmptyResultError,
|
|
28
|
+
ArgumentError,
|
|
29
|
+
AdapterLoadError,
|
|
30
|
+
CommandExecutionError,
|
|
31
|
+
} from './errors.js';
|
|
32
|
+
import { checkDaemonStatus } from './browser/discover.js';
|
|
33
|
+
|
|
34
|
+
export function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown {
|
|
35
|
+
if (argType !== 'bool') return value;
|
|
36
|
+
if (typeof value === 'boolean') return value;
|
|
37
|
+
if (value == null || value === '') return false;
|
|
38
|
+
|
|
39
|
+
const normalized = String(value).trim().toLowerCase();
|
|
40
|
+
if (normalized === 'true') return true;
|
|
41
|
+
if (normalized === 'false') return false;
|
|
42
|
+
|
|
43
|
+
throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
|
|
44
|
+
}
|
|
20
45
|
|
|
21
46
|
/**
|
|
22
47
|
* Register a single CliCommand as a Commander subcommand.
|
|
@@ -24,7 +49,8 @@ import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
|
|
|
24
49
|
export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): void {
|
|
25
50
|
if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) return;
|
|
26
51
|
|
|
27
|
-
const
|
|
52
|
+
const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
|
|
53
|
+
const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
|
|
28
54
|
|
|
29
55
|
// Register positional args first, then named options
|
|
30
56
|
const positionalArgs: typeof cmd.args = [];
|
|
@@ -51,24 +77,29 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
51
77
|
const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts as Record<string, unknown> : {};
|
|
52
78
|
const startTime = Date.now();
|
|
53
79
|
|
|
54
|
-
// ── Collect kwargs ──────────────────────────────────────────────────
|
|
55
|
-
const kwargs: Record<string, unknown> = {};
|
|
56
|
-
for (let i = 0; i < positionalArgs.length; i++) {
|
|
57
|
-
const v = actionArgs[i];
|
|
58
|
-
if (v !== undefined) kwargs[positionalArgs[i].name] = v;
|
|
59
|
-
}
|
|
60
|
-
for (const arg of cmd.args) {
|
|
61
|
-
if (arg.positional) continue;
|
|
62
|
-
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
|
|
63
|
-
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
64
|
-
if (v !== undefined) kwargs[arg.name] = v;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
80
|
// ── Execute + render ────────────────────────────────────────────────
|
|
68
81
|
try {
|
|
82
|
+
// ── Collect kwargs ────────────────────────────────────────────────
|
|
83
|
+
const kwargs: Record<string, unknown> = {};
|
|
84
|
+
for (let i = 0; i < positionalArgs.length; i++) {
|
|
85
|
+
const v = actionArgs[i];
|
|
86
|
+
if (v !== undefined) kwargs[positionalArgs[i].name] = v;
|
|
87
|
+
}
|
|
88
|
+
for (const arg of cmd.args) {
|
|
89
|
+
if (arg.positional) continue;
|
|
90
|
+
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
|
|
91
|
+
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
92
|
+
if (v !== undefined) kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
const verbose = optionsRecord.verbose === true;
|
|
70
96
|
const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
71
97
|
if (verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
98
|
+
if (cmd.deprecated) {
|
|
99
|
+
const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
|
|
100
|
+
const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
|
|
101
|
+
console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
|
|
102
|
+
}
|
|
72
103
|
|
|
73
104
|
const result = await executeCommand(cmd, kwargs, verbose);
|
|
74
105
|
|
|
@@ -85,20 +116,153 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
85
116
|
footerExtra: resolved.footerExtra?.(kwargs),
|
|
86
117
|
});
|
|
87
118
|
} catch (err) {
|
|
88
|
-
|
|
89
|
-
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
90
|
-
console.error(chalk.red(`${icon} ${err.message}`));
|
|
91
|
-
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
92
|
-
} else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
|
|
93
|
-
console.error(chalk.red(err.stack));
|
|
94
|
-
} else {
|
|
95
|
-
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
96
|
-
}
|
|
119
|
+
await renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
97
120
|
process.exitCode = 1;
|
|
98
121
|
}
|
|
99
122
|
});
|
|
100
123
|
}
|
|
101
124
|
|
|
125
|
+
// ── Error rendering ──────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
|
|
128
|
+
|
|
129
|
+
/** Pattern-based classifier for untyped errors thrown by adapters. */
|
|
130
|
+
function classifyGenericError(msg: string): 'auth' | 'http' | 'not-found' | 'other' {
|
|
131
|
+
const m = msg.toLowerCase();
|
|
132
|
+
if (/not logged in|login required|please log in|未登录|请先登录|authentication required|cookie expired/.test(m)) return 'auth';
|
|
133
|
+
// Match "HTTP 404", "status: 500", "status 403", bare "404 Not Found", etc.
|
|
134
|
+
if (/\b(status[: ]+)?[45]\d{2}\b|http[/ ][45]\d{2}/.test(m)) return 'http';
|
|
135
|
+
if (/not found|未找到|could not find|no .+ found/.test(m)) return 'not-found';
|
|
136
|
+
return 'other';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Render a status line for BrowserConnectError based on real-time or kind-derived state. */
|
|
140
|
+
function renderBridgeStatus(running: boolean, extensionConnected: boolean): void {
|
|
141
|
+
const ok = chalk.green('✓');
|
|
142
|
+
const fail = chalk.red('✗');
|
|
143
|
+
console.error(` Daemon ${running ? ok : fail} ${running ? 'running' : 'not running'}`);
|
|
144
|
+
console.error(` Extension ${extensionConnected ? ok : fail} ${extensionConnected ? 'connected' : 'not connected'}`);
|
|
145
|
+
console.error();
|
|
146
|
+
if (!running) {
|
|
147
|
+
console.error(chalk.yellow(' Run the command again — daemon should auto-start.'));
|
|
148
|
+
console.error(chalk.dim(' Still failing? Run: opencli doctor'));
|
|
149
|
+
} else if (!extensionConnected) {
|
|
150
|
+
console.error(chalk.yellow(' Install the Browser Bridge extension to continue:'));
|
|
151
|
+
console.error(chalk.dim(' 1. Download from github.com/jackwener/opencli/releases'));
|
|
152
|
+
console.error(chalk.dim(' 2. chrome://extensions → Enable Developer Mode → Load unpacked'));
|
|
153
|
+
} else {
|
|
154
|
+
console.error(chalk.yellow(' Connection failed despite extension being active.'));
|
|
155
|
+
console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function renderError(err: unknown, cmdName: string, verbose: boolean): Promise<void> {
|
|
160
|
+
// ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
|
|
161
|
+
if (err instanceof BrowserConnectError) {
|
|
162
|
+
console.error(chalk.red('🔌 Browser Bridge not connected'));
|
|
163
|
+
console.error();
|
|
164
|
+
try {
|
|
165
|
+
// 300ms matches execution.ts — localhost responds in <50ms when running.
|
|
166
|
+
const status = await checkDaemonStatus({ timeout: 300 });
|
|
167
|
+
renderBridgeStatus(status.running, status.extensionConnected);
|
|
168
|
+
} catch (_statusErr) {
|
|
169
|
+
// checkDaemonStatus itself failed — derive best-guess state from kind.
|
|
170
|
+
const running = err.kind !== 'daemon-not-running';
|
|
171
|
+
const extensionConnected = err.kind === 'command-failed';
|
|
172
|
+
renderBridgeStatus(running, extensionConnected);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── AuthRequiredError ─────────────────────────────────────────────────
|
|
178
|
+
if (err instanceof AuthRequiredError) {
|
|
179
|
+
console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
|
|
180
|
+
// Respect custom hints set by the adapter; fall back to generic guidance.
|
|
181
|
+
console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── TimeoutError ──────────────────────────────────────────────────────
|
|
186
|
+
if (err instanceof TimeoutError) {
|
|
187
|
+
console.error(chalk.red(`⏱ ${err.message}`));
|
|
188
|
+
console.error(chalk.yellow('→ Try again, or raise the limit:'));
|
|
189
|
+
console.error(chalk.dim(` OPENCLI_BROWSER_COMMAND_TIMEOUT=60 ${cmdName}`));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── SelectorError / EmptyResultError: likely outdated adapter ─────────
|
|
194
|
+
if (err instanceof SelectorError || err instanceof EmptyResultError) {
|
|
195
|
+
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
196
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
197
|
+
console.error(chalk.yellow('→ The page structure may have changed — this adapter may be outdated.'));
|
|
198
|
+
console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
|
|
199
|
+
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── ArgumentError ─────────────────────────────────────────────────────
|
|
204
|
+
if (err instanceof ArgumentError) {
|
|
205
|
+
console.error(chalk.red(`❌ ${err.message}`));
|
|
206
|
+
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── AdapterLoadError ──────────────────────────────────────────────────
|
|
211
|
+
if (err instanceof AdapterLoadError) {
|
|
212
|
+
console.error(chalk.red(`📦 ${err.message}`));
|
|
213
|
+
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── CommandExecutionError ─────────────────────────────────────────────
|
|
218
|
+
if (err instanceof CommandExecutionError) {
|
|
219
|
+
console.error(chalk.red(`💥 ${err.message}`));
|
|
220
|
+
if (err.hint) {
|
|
221
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
222
|
+
} else {
|
|
223
|
+
console.error(chalk.dim(` Add --verbose for details, or report: ${ISSUES_URL}`));
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Other typed CliError (fallback for future codes) ──────────────────
|
|
229
|
+
if (err instanceof CliError) {
|
|
230
|
+
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
231
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
232
|
+
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Generic Error from adapters: classify by message pattern ──────────
|
|
237
|
+
const msg = getErrorMessage(err);
|
|
238
|
+
const kind = classifyGenericError(msg);
|
|
239
|
+
|
|
240
|
+
if (kind === 'auth') {
|
|
241
|
+
console.error(chalk.red(`🔒 ${msg}`));
|
|
242
|
+
console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (kind === 'http') {
|
|
246
|
+
console.error(chalk.red(`🌐 ${msg}`));
|
|
247
|
+
console.error(chalk.yellow('→ Check your login status, or the site may be temporarily unavailable.'));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (kind === 'not-found') {
|
|
251
|
+
console.error(chalk.red(`📭 ${msg}`));
|
|
252
|
+
console.error(chalk.yellow('→ The resource was not found. The adapter or page structure may have changed.'));
|
|
253
|
+
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Unknown error: show stack in verbose mode ─────────────────────────
|
|
258
|
+
if (verbose && err instanceof Error && err.stack) {
|
|
259
|
+
console.error(chalk.red(err.stack));
|
|
260
|
+
} else {
|
|
261
|
+
console.error(chalk.red(`💥 Unexpected error: ${msg}`));
|
|
262
|
+
console.error(chalk.dim(` Run with --verbose for details, or report: ${ISSUES_URL}`));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
102
266
|
/**
|
|
103
267
|
* Register all commands from the registry onto a Commander program.
|
|
104
268
|
*/
|
package/src/daemon.ts
CHANGED
|
@@ -29,6 +29,7 @@ const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
|
29
29
|
// ─── State ───────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
31
|
let extensionWs: WebSocket | null = null;
|
|
32
|
+
let extensionVersion: string | null = null;
|
|
32
33
|
const pending = new Map<string, {
|
|
33
34
|
resolve: (data: unknown) => void;
|
|
34
35
|
reject: (error: Error) => void;
|
|
@@ -117,6 +118,7 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
117
118
|
jsonResponse(res, 200, {
|
|
118
119
|
ok: true,
|
|
119
120
|
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
121
|
+
extensionVersion,
|
|
120
122
|
pending: pending.size,
|
|
121
123
|
});
|
|
122
124
|
return;
|
|
@@ -222,6 +224,12 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
222
224
|
try {
|
|
223
225
|
const msg = JSON.parse(data.toString());
|
|
224
226
|
|
|
227
|
+
// Handle hello message from extension (version handshake)
|
|
228
|
+
if (msg.type === 'hello') {
|
|
229
|
+
extensionVersion = typeof msg.version === 'string' ? msg.version : null;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
225
233
|
// Handle log messages from extension
|
|
226
234
|
if (msg.type === 'log') {
|
|
227
235
|
const prefix = msg.level === 'error' ? '❌' : msg.level === 'warn' ? '⚠️' : '📋';
|
|
@@ -247,6 +255,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
247
255
|
clearInterval(heartbeatInterval);
|
|
248
256
|
if (extensionWs === ws) {
|
|
249
257
|
extensionWs = null;
|
|
258
|
+
extensionVersion = null;
|
|
250
259
|
// Reject all pending requests since the extension is gone
|
|
251
260
|
for (const [id, p] of pending) {
|
|
252
261
|
clearTimeout(p.timer);
|
|
@@ -258,7 +267,16 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
258
267
|
|
|
259
268
|
ws.on('error', () => {
|
|
260
269
|
clearInterval(heartbeatInterval);
|
|
261
|
-
if (extensionWs === ws)
|
|
270
|
+
if (extensionWs === ws) {
|
|
271
|
+
extensionWs = null;
|
|
272
|
+
extensionVersion = null;
|
|
273
|
+
// Reject pending requests in case 'close' does not follow this 'error'
|
|
274
|
+
for (const [, p] of pending) {
|
|
275
|
+
clearTimeout(p.timer);
|
|
276
|
+
p.reject(new Error('Extension disconnected'));
|
|
277
|
+
}
|
|
278
|
+
pending.clear();
|
|
279
|
+
}
|
|
262
280
|
});
|
|
263
281
|
});
|
|
264
282
|
|
package/src/discovery.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
|
|
|
23
23
|
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
24
24
|
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
25
25
|
|
|
26
|
-
import type
|
|
26
|
+
import { type YamlCliDefinition, parseYamlArgs } from './yaml-schema.js';
|
|
27
27
|
|
|
28
28
|
function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
|
|
29
29
|
if (!rawStrategy) return fallback;
|
|
@@ -77,6 +77,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
77
77
|
pipeline: entry.pipeline,
|
|
78
78
|
timeoutSeconds: entry.timeout,
|
|
79
79
|
source: `manifest:${entry.site}/${entry.name}`,
|
|
80
|
+
deprecated: entry.deprecated,
|
|
81
|
+
replacedBy: entry.replacedBy,
|
|
80
82
|
navigateBefore: entry.navigateBefore,
|
|
81
83
|
};
|
|
82
84
|
registerCommand(cmd);
|
|
@@ -96,6 +98,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
96
98
|
columns: entry.columns,
|
|
97
99
|
timeoutSeconds: entry.timeout,
|
|
98
100
|
source: modulePath,
|
|
101
|
+
deprecated: entry.deprecated,
|
|
102
|
+
replacedBy: entry.replacedBy,
|
|
99
103
|
navigateBefore: entry.navigateBefore,
|
|
100
104
|
_lazy: true,
|
|
101
105
|
_modulePath: modulePath,
|
|
@@ -158,20 +162,7 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
|
|
|
158
162
|
const strategy = parseStrategy(strategyStr);
|
|
159
163
|
const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
|
|
160
164
|
|
|
161
|
-
const args
|
|
162
|
-
if (cliDef.args && typeof cliDef.args === 'object') {
|
|
163
|
-
for (const [argName, argDef] of Object.entries(cliDef.args)) {
|
|
164
|
-
args.push({
|
|
165
|
-
name: argName,
|
|
166
|
-
type: argDef?.type ?? 'str',
|
|
167
|
-
default: argDef?.default,
|
|
168
|
-
required: argDef?.required ?? false,
|
|
169
|
-
positional: argDef?.positional ?? false,
|
|
170
|
-
help: argDef?.description ?? argDef?.help ?? '',
|
|
171
|
-
choices: argDef?.choices,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
165
|
+
const args = parseYamlArgs(cliDef.args);
|
|
175
166
|
|
|
176
167
|
const cmd: CliCommand = {
|
|
177
168
|
site,
|
|
@@ -185,6 +176,8 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
|
|
|
185
176
|
pipeline: cliDef.pipeline,
|
|
186
177
|
timeoutSeconds: cliDef.timeout,
|
|
187
178
|
source: filePath,
|
|
179
|
+
deprecated: (cliDef as Record<string, unknown>).deprecated as boolean | string | undefined,
|
|
180
|
+
replacedBy: (cliDef as Record<string, unknown>).replacedBy as string | undefined,
|
|
188
181
|
navigateBefore: cliDef.navigateBefore,
|
|
189
182
|
};
|
|
190
183
|
|
package/src/doctor.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { checkDaemonStatus } from './browser/discover.js';
|
|
|
11
11
|
import { BrowserBridge } from './browser/index.js';
|
|
12
12
|
import { listSessions } from './browser/daemon-client.js';
|
|
13
13
|
import { getErrorMessage } from './errors.js';
|
|
14
|
+
import { getRuntimeLabel } from './runtime-detect.js';
|
|
14
15
|
|
|
15
16
|
export type DoctorOptions = {
|
|
16
17
|
fix?: boolean;
|
|
@@ -30,6 +31,7 @@ export type DoctorReport = {
|
|
|
30
31
|
cliVersion?: string;
|
|
31
32
|
daemonRunning: boolean;
|
|
32
33
|
extensionConnected: boolean;
|
|
34
|
+
extensionVersion?: string;
|
|
33
35
|
connectivity?: ConnectivityResult;
|
|
34
36
|
sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
|
|
35
37
|
issues: string[];
|
|
@@ -94,10 +96,18 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
94
96
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
if (status.extensionVersion && opts.cliVersion && status.extensionVersion !== opts.cliVersion) {
|
|
100
|
+
issues.push(
|
|
101
|
+
`Extension version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
|
|
102
|
+
' Download the latest extension from: https://github.com/jackwener/opencli/releases',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
return {
|
|
98
107
|
cliVersion: opts.cliVersion,
|
|
99
108
|
daemonRunning: status.running,
|
|
100
109
|
extensionConnected: status.extensionConnected,
|
|
110
|
+
extensionVersion: status.extensionVersion,
|
|
101
111
|
connectivity,
|
|
102
112
|
sessions,
|
|
103
113
|
issues,
|
|
@@ -105,7 +115,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
108
|
-
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
|
118
|
+
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
|
|
109
119
|
|
|
110
120
|
// Daemon status
|
|
111
121
|
const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
|
|
@@ -113,7 +123,8 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
113
123
|
|
|
114
124
|
// Extension status
|
|
115
125
|
const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
|
|
116
|
-
|
|
126
|
+
const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
|
|
127
|
+
lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
|
|
117
128
|
|
|
118
129
|
// Connectivity
|
|
119
130
|
if (report.connectivity) {
|
|
@@ -6,12 +6,17 @@ import { afterEach, describe, expect, it } from 'vitest';
|
|
|
6
6
|
import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
|
|
7
7
|
|
|
8
8
|
const servers: http.Server[] = [];
|
|
9
|
+
const tempDirs: string[] = [];
|
|
9
10
|
|
|
10
11
|
afterEach(async () => {
|
|
11
12
|
await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
|
|
12
13
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
13
14
|
})));
|
|
14
15
|
servers.length = 0;
|
|
16
|
+
for (const dir of tempDirs) {
|
|
17
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
18
|
+
}
|
|
19
|
+
tempDirs.length = 0;
|
|
15
20
|
});
|
|
16
21
|
|
|
17
22
|
async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> {
|
|
@@ -25,7 +30,9 @@ async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'
|
|
|
25
30
|
return `http://${hostname}:${address.port}`;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
// Windows Defender can briefly lock newly-written .tmp files, causing EPERM.
|
|
34
|
+
// Retry once to handle this flakiness.
|
|
35
|
+
describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
|
|
29
36
|
it('resolves relative redirects against the original URL', () => {
|
|
30
37
|
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
31
38
|
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
@@ -45,7 +52,8 @@ describe('download helpers', () => {
|
|
|
45
52
|
res.end();
|
|
46
53
|
});
|
|
47
54
|
|
|
48
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
55
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
56
|
+
tempDirs.push(tempDir);
|
|
49
57
|
const destPath = path.join(tempDir, 'file.txt');
|
|
50
58
|
const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
|
|
51
59
|
|
|
@@ -71,7 +79,8 @@ describe('download helpers', () => {
|
|
|
71
79
|
res.end();
|
|
72
80
|
});
|
|
73
81
|
|
|
74
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
82
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
83
|
+
tempDirs.push(tempDir);
|
|
75
84
|
const destPath = path.join(tempDir, 'redirect.txt');
|
|
76
85
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
|
|
77
86
|
|
|
@@ -94,7 +103,8 @@ describe('download helpers', () => {
|
|
|
94
103
|
res.end();
|
|
95
104
|
});
|
|
96
105
|
|
|
97
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
106
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
107
|
+
tempDirs.push(tempDir);
|
|
98
108
|
const destPath = path.join(tempDir, 'redirect-header.txt');
|
|
99
109
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, {
|
|
100
110
|
headers: { Cookie: 'sid=header-cookie' },
|
package/src/download/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ import * as path from 'node:path';
|
|
|
8
8
|
import * as https from 'node:https';
|
|
9
9
|
import * as http from 'node:http';
|
|
10
10
|
import * as os from 'node:os';
|
|
11
|
+
import { Transform } from 'node:stream';
|
|
12
|
+
import { pipeline } from 'node:stream/promises';
|
|
11
13
|
import { URL } from 'node:url';
|
|
12
14
|
import type { ProgressBar } from './progress.js';
|
|
13
15
|
import { isBinaryInstalled } from '../external.js';
|
|
@@ -99,74 +101,84 @@ export async function httpDownload(
|
|
|
99
101
|
requestHeaders['Cookie'] = cookies;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
// Ensure directory exists
|
|
103
|
-
const dir = path.dirname(destPath);
|
|
104
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
-
|
|
106
104
|
const tempPath = `${destPath}.tmp`;
|
|
107
|
-
|
|
105
|
+
let settled = false;
|
|
108
106
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (redirectCount >= maxRedirects) {
|
|
115
|
-
resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
119
|
-
const originalHost = new URL(url).hostname;
|
|
120
|
-
const redirectHost = new URL(redirectUrl).hostname;
|
|
121
|
-
// Do not forward cookies when a redirect crosses host boundaries.
|
|
122
|
-
const redirectOptions = originalHost === redirectHost
|
|
123
|
-
? options
|
|
124
|
-
: { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
|
|
125
|
-
httpDownload(
|
|
126
|
-
redirectUrl,
|
|
127
|
-
destPath,
|
|
128
|
-
redirectOptions,
|
|
129
|
-
redirectCount + 1,
|
|
130
|
-
).then(resolve);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
107
|
+
const finish = (result: { success: boolean; size: number; error?: string }) => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
resolve(result);
|
|
111
|
+
};
|
|
133
112
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
113
|
+
const cleanupTempFile = async () => {
|
|
114
|
+
try {
|
|
115
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
116
|
+
} catch {
|
|
117
|
+
// Ignore cleanup errors so the original failure is preserved.
|
|
139
118
|
}
|
|
119
|
+
};
|
|
140
120
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
121
|
+
const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
|
|
122
|
+
void (async () => {
|
|
123
|
+
// Handle redirects before creating any file handles.
|
|
124
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
125
|
+
response.resume();
|
|
126
|
+
if (redirectCount >= maxRedirects) {
|
|
127
|
+
finish({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
131
|
+
const originalHost = new URL(url).hostname;
|
|
132
|
+
const redirectHost = new URL(redirectUrl).hostname;
|
|
133
|
+
const redirectOptions = originalHost === redirectHost
|
|
134
|
+
? options
|
|
135
|
+
: { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
|
|
136
|
+
finish(await httpDownload(
|
|
137
|
+
redirectUrl,
|
|
138
|
+
destPath,
|
|
139
|
+
redirectOptions,
|
|
140
|
+
redirectCount + 1,
|
|
141
|
+
));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
148
144
|
|
|
149
|
-
|
|
145
|
+
if (response.statusCode !== 200) {
|
|
146
|
+
response.resume();
|
|
147
|
+
finish({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
|
152
|
+
let received = 0;
|
|
153
|
+
const progressStream = new Transform({
|
|
154
|
+
transform(chunk, _encoding, callback) {
|
|
155
|
+
received += chunk.length;
|
|
156
|
+
if (onProgress) onProgress(received, totalSize);
|
|
157
|
+
callback(null, chunk);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
163
|
+
await pipeline(response, progressStream, fs.createWriteStream(tempPath));
|
|
164
|
+
await fs.promises.rename(tempPath, destPath);
|
|
165
|
+
finish({ success: true, size: received });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
await cleanupTempFile();
|
|
168
|
+
finish({ success: false, size: 0, error: getErrorMessage(err) });
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
157
171
|
});
|
|
158
172
|
|
|
159
173
|
request.on('error', (err) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
void (async () => {
|
|
175
|
+
await cleanupTempFile();
|
|
176
|
+
finish({ success: false, size: 0, error: err.message });
|
|
177
|
+
})();
|
|
163
178
|
});
|
|
164
179
|
|
|
165
180
|
request.on('timeout', () => {
|
|
166
|
-
request.destroy();
|
|
167
|
-
file.close();
|
|
168
|
-
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
169
|
-
resolve({ success: false, size: 0, error: 'Timeout' });
|
|
181
|
+
request.destroy(new Error('Timeout'));
|
|
170
182
|
});
|
|
171
183
|
});
|
|
172
184
|
}
|