@sovovs/bycli 1.8.0 → 2.0.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/cli-manifest.json +0 -61
- package/dist/src/adapter-shadow.js +2 -2
- package/dist/src/browser/analyze.d.ts +10 -99
- package/dist/src/browser/analyze.js +10 -226
- package/dist/src/browser/cdp.js +64 -7
- package/dist/src/browser/navigation-guard.d.ts +78 -0
- package/dist/src/browser/navigation-guard.js +142 -0
- package/dist/src/browser/navigation-guard.test.d.ts +1 -0
- package/dist/src/browser/url-policy.d.ts +89 -0
- package/dist/src/browser/url-policy.js +311 -0
- package/dist/src/browser/url-policy.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.js +2 -2
- package/dist/src/cli.js +10 -13
- package/dist/src/config-paths.d.ts +19 -0
- package/dist/src/config-paths.js +31 -0
- package/dist/src/daemon.js +240 -0
- package/dist/src/discovery.d.ts +5 -3
- package/dist/src/discovery.js +8 -6
- package/dist/src/execution.js +8 -3
- package/dist/src/main.js +18 -2
- package/dist/src/plugin.js +11 -8
- package/dist/src/recorder/highlevel/analyze.d.ts +29 -0
- package/dist/src/recorder/highlevel/analyze.js +91 -0
- package/dist/src/recorder/highlevel/init.d.ts +125 -0
- package/dist/src/recorder/highlevel/init.js +279 -0
- package/dist/src/recorder/highlevel/init.test.d.ts +1 -0
- package/dist/src/recorder/highlevel/verify.d.ts +73 -0
- package/dist/src/recorder/highlevel/verify.js +74 -0
- package/dist/src/recorder/http/analyze-runner.d.ts +28 -0
- package/dist/src/recorder/http/analyze-runner.js +78 -0
- package/dist/src/recorder/http/analyze-runner.test.d.ts +1 -0
- package/dist/src/recorder/http/handlers.d.ts +51 -0
- package/dist/src/recorder/http/handlers.js +219 -0
- package/dist/src/recorder/http/wrapper-config.d.ts +20 -0
- package/dist/src/recorder/http/wrapper-config.js +82 -0
- package/dist/src/recorder/http/wrapper-envelope.d.ts +31 -0
- package/dist/src/recorder/http/wrapper-envelope.js +51 -0
- package/dist/src/recorder/http/wrapper-gates.d.ts +16 -0
- package/dist/src/recorder/http/wrapper-gates.js +28 -0
- package/dist/src/recorder/http/wrapper-metrics.d.ts +1 -0
- package/dist/src/recorder/http/wrapper-metrics.js +5 -0
- package/dist/src/recorder/http/wrapper-registry.d.ts +50 -0
- package/dist/src/recorder/http/wrapper-registry.js +98 -0
- package/dist/src/recorder/http/wrapper-server.d.ts +59 -0
- package/dist/src/recorder/http/wrapper-server.js +240 -0
- package/dist/src/recorder/http/wrapper.test.d.ts +1 -0
- package/dist/src/recorder/observability/logger.d.ts +5 -0
- package/dist/src/recorder/observability/logger.js +13 -0
- package/dist/src/recorder/runner/config.d.ts +34 -0
- package/dist/src/recorder/runner/config.js +84 -0
- package/dist/src/recorder/runner/config.test.d.ts +1 -0
- package/dist/src/recorder/runner/reap.d.ts +79 -0
- package/dist/src/recorder/runner/reap.js +230 -0
- package/dist/src/recorder/runner/reap.test.d.ts +1 -0
- package/dist/src/recorder/runner/runner-port.d.ts +95 -0
- package/dist/src/recorder/runner/runner-port.js +518 -0
- package/dist/src/recorder/runner/runner-port.test.d.ts +1 -0
- package/dist/src/recorder/runner/session-keys.d.ts +28 -0
- package/dist/src/recorder/runner/session-keys.js +72 -0
- package/dist/src/recorder/runner/session-keys.test.d.ts +1 -0
- package/dist/src/recorder/runner/verify-runner-main.d.ts +95 -0
- package/dist/src/recorder/runner/verify-runner-main.js +275 -0
- package/dist/src/recorder/runner/verify-runner-main.test.d.ts +1 -0
- package/package.json +8 -5
- package/scripts/m1-daemon-navigate-drive.mjs +138 -0
- package/scripts/m1-extension-spike.mjs +237 -0
- package/scripts/m1-live-spike.mjs +156 -0
- package/scripts/m6b-connect-back-spike.sh +31 -0
- package/scripts/recorder.sh +186 -0
- package/clis/12306/utils.test.js +0 -331
- package/clis/1688/assets.test.js +0 -39
- package/clis/1688/download.test.js +0 -31
- package/clis/1688/item.test.js +0 -67
- package/clis/1688/search.test.js +0 -75
- package/clis/1688/shared.test.js +0 -57
- package/clis/1688/store.test.js +0 -62
- package/clis/36kr/article.test.js +0 -46
- package/clis/36kr/hot.test.js +0 -15
- package/clis/36kr/news.test.js +0 -85
- package/clis/51job/utils.test.js +0 -69
- package/clis/_atlassian/shared.test.js +0 -170
- package/clis/aibase/news.test.js +0 -59
- package/clis/amazon/bestsellers.test.js +0 -29
- package/clis/amazon/discussion.test.js +0 -124
- package/clis/amazon/offer.test.js +0 -29
- package/clis/amazon/product.test.js +0 -24
- package/clis/amazon/rankings.test.js +0 -41
- package/clis/amazon/search.test.js +0 -22
- package/clis/amazon/shared.test.js +0 -44
- package/clis/apple-podcasts/commands.test.js +0 -119
- package/clis/apple-podcasts/utils.test.js +0 -57
- package/clis/arxiv/arxiv.test.js +0 -112
- package/clis/baidu-scholar/search.test.js +0 -23
- package/clis/barchart/greeks.test.js +0 -138
- package/clis/bilibili/comment.test.js +0 -153
- package/clis/bilibili/comments.test.js +0 -142
- package/clis/bilibili/dynamic.test.js +0 -68
- package/clis/bilibili/hot.test.js +0 -17
- package/clis/bilibili/subtitle.test.js +0 -196
- package/clis/bilibili/summary.test.js +0 -210
- package/clis/bilibili/utils.test.js +0 -61
- package/clis/bilibili/video.test.js +0 -132
- package/clis/binance/commands.test.js +0 -70
- package/clis/bloomberg/utils.test.js +0 -129
- package/clis/booking/booking.test.js +0 -356
- package/clis/boss/chatlist.test.js +0 -211
- package/clis/boss/chatmsg.test.js +0 -230
- package/clis/boss/search.test.js +0 -78
- package/clis/boss/utils.test.js +0 -34
- package/clis/brave/search.test.js +0 -76
- package/clis/chaoxing/utils.test.js +0 -45
- package/clis/chatgpt/commands.test.js +0 -45
- package/clis/chatgpt/envelope.test.js +0 -108
- package/clis/chatgpt/image.test.js +0 -188
- package/clis/chatgpt/utils.test.js +0 -330
- package/clis/chatgpt-app/ax.test.js +0 -35
- package/clis/chatwise/composer.test.js +0 -186
- package/clis/chess/analyze.test.js +0 -79
- package/clis/chess/game.test.js +0 -178
- package/clis/chess/games.test.js +0 -164
- package/clis/chess/stats.test.js +0 -79
- package/clis/chess/utils.test.js +0 -230
- package/clis/claude/ask.test.js +0 -338
- package/clis/claude/commands.test.js +0 -118
- package/clis/claude/utils.test.js +0 -148
- package/clis/cnki/search.test.js +0 -18
- package/clis/codex/sidebar.test.js +0 -329
- package/clis/coingecko/coingecko.test.js +0 -109
- package/clis/confluence/commands.test.js +0 -195
- package/clis/coupang/coupang.test.js +0 -159
- package/clis/coupang/utils.test.js +0 -62
- package/clis/ctrip/ctrip.test.js +0 -719
- package/clis/dblp/dblp.test.js +0 -397
- package/clis/deepseek/ask.test.js +0 -312
- package/clis/deepseek/detail.test.js +0 -81
- package/clis/deepseek/send.test.js +0 -107
- package/clis/deepseek/utils.test.js +0 -370
- package/clis/defillama/defillama.test.js +0 -99
- package/clis/devto/devto.test.js +0 -236
- package/clis/dianping/dianping.test.js +0 -578
- package/clis/douban/download.test.js +0 -170
- package/clis/douban/movie-hot.test.js +0 -14
- package/clis/douban/search.test.js +0 -11
- package/clis/douban/subject.test.js +0 -11
- package/clis/douban/utils.test.js +0 -438
- package/clis/doubao/detail.test.js +0 -42
- package/clis/doubao/history.test.js +0 -37
- package/clis/doubao/utils.test.js +0 -344
- package/clis/douyin/_shared/browser-fetch.test.js +0 -64
- package/clis/douyin/_shared/creation-id.test.js +0 -22
- package/clis/douyin/_shared/imagex-upload.test.js +0 -87
- package/clis/douyin/_shared/sts2.test.js +0 -27
- package/clis/douyin/_shared/text-extra.test.js +0 -37
- package/clis/douyin/_shared/timing.test.js +0 -28
- package/clis/douyin/_shared/tos-upload-short-read.test.js +0 -83
- package/clis/douyin/_shared/tos-upload.test.js +0 -229
- package/clis/douyin/_shared/transcode.test.js +0 -91
- package/clis/douyin/_shared/vod-upload.test.js +0 -38
- package/clis/douyin/activities.test.js +0 -55
- package/clis/douyin/collections.test.js +0 -45
- package/clis/douyin/delete.test.js +0 -100
- package/clis/douyin/draft.test.js +0 -335
- package/clis/douyin/drafts.test.js +0 -11
- package/clis/douyin/hashtag.test.js +0 -171
- package/clis/douyin/location.test.js +0 -23
- package/clis/douyin/profile.test.js +0 -46
- package/clis/douyin/publish-upload-id.test.js +0 -170
- package/clis/douyin/publish.test.js +0 -38
- package/clis/douyin/stats.test.js +0 -22
- package/clis/douyin/update.test.js +0 -11
- package/clis/douyin/user-videos.test.js +0 -151
- package/clis/douyin/videos.test.js +0 -54
- package/clis/duckduckgo/search.test.js +0 -128
- package/clis/duckduckgo/suggest.test.js +0 -66
- package/clis/eastmoney/hot-rank.test.js +0 -59
- package/clis/endoflife/endoflife.test.js +0 -51
- package/clis/facebook/feed.test.js +0 -169
- package/clis/facebook/marketplace.test.js +0 -91
- package/clis/facebook/notifications.test.js +0 -458
- package/clis/facebook/search.test.js +0 -55
- package/clis/flathub/flathub.test.js +0 -90
- package/clis/flomo/memos.test.js +0 -144
- package/clis/gemini/ask.test.js +0 -100
- package/clis/gemini/deep-research-result.test.js +0 -155
- package/clis/gemini/deep-research.test.js +0 -181
- package/clis/gemini/reply-state.test.js +0 -671
- package/clis/gemini/utils.test.js +0 -198
- package/clis/geogebra/geogebra.test.js +0 -175
- package/clis/gitee/search.test.js +0 -65
- package/clis/gitee/user.test.js +0 -63
- package/clis/google/utils.test.js +0 -75
- package/clis/google-scholar/cite.test.js +0 -47
- package/clis/google-scholar/profile.test.js +0 -49
- package/clis/google-scholar/search.test.js +0 -71
- package/clis/goproxy/goproxy.test.js +0 -103
- package/clis/gov-law/commands.test.js +0 -39
- package/clis/gov-policy/gov-policy.test.js +0 -224
- package/clis/grok/ask.test.js +0 -29
- package/clis/grok/utils.test.js +0 -103
- package/clis/hackernews/hackernews.test.js +0 -132
- package/clis/hf/hf.test.js +0 -16
- package/clis/hupu/hot.test.js +0 -224
- package/clis/imdb/utils.test.js +0 -88
- package/clis/indeed/indeed.test.js +0 -375
- package/clis/instagram/_shared/private-publish.test.js +0 -705
- package/clis/instagram/_shared/protocol-capture.test.js +0 -114
- package/clis/instagram/download.test.js +0 -118
- package/clis/instagram/note.test.js +0 -53
- package/clis/instagram/post.test.js +0 -1647
- package/clis/instagram/reel.test.js +0 -167
- package/clis/instagram/story.test.js +0 -134
- package/clis/jd/commands.test.js +0 -41
- package/clis/jd/item.test.js +0 -354
- package/clis/jianyu/search.test.js +0 -153
- package/clis/jianyu/shared/procurement-contract.test.js +0 -72
- package/clis/jianyu/shared/procurement-detail.test.js +0 -84
- package/clis/jike/read.test.js +0 -86
- package/clis/jira/commands.test.js +0 -287
- package/clis/juejin/search.js +0 -123
- package/clis/juejin/search.test.js +0 -98
- package/clis/lesswrong/frontpage.test.js +0 -37
- package/clis/lichess/lichess.test.js +0 -85
- package/clis/linkedin/connect.test.js +0 -213
- package/clis/linkedin/inbox.test.js +0 -152
- package/clis/linkedin/job-detail.test.js +0 -38
- package/clis/linkedin/jobs-preferences.test.js +0 -43
- package/clis/linkedin/people-search.test.js +0 -216
- package/clis/linkedin/post-analytics.test.js +0 -40
- package/clis/linkedin/posts.test.js +0 -40
- package/clis/linkedin/profile-analytics.test.js +0 -67
- package/clis/linkedin/profile-experience.test.js +0 -152
- package/clis/linkedin/profile-projects.test.js +0 -111
- package/clis/linkedin/profile-read.test.js +0 -77
- package/clis/linkedin/safe-send.test.js +0 -204
- package/clis/linkedin/salesnav-inbox.test.js +0 -113
- package/clis/linkedin/salesnav-message.test.js +0 -172
- package/clis/linkedin/salesnav-search.test.js +0 -76
- package/clis/linkedin/salesnav-thread.test.js +0 -79
- package/clis/linkedin/search.test.js +0 -222
- package/clis/linkedin/sent-invitations.test.js +0 -62
- package/clis/linkedin/services-read.test.js +0 -105
- package/clis/linkedin/thread-snapshot.test.js +0 -89
- package/clis/linkedin/timeline.test.js +0 -81
- package/clis/linkedin-learning/course.test.js +0 -114
- package/clis/linkedin-learning/search.test.js +0 -144
- package/clis/linkedin-learning/trending.test.js +0 -123
- package/clis/linux-do/feed.test.js +0 -153
- package/clis/linux-do/topic-content.test.js +0 -59
- package/clis/lobsters/lobsters.test.js +0 -169
- package/clis/notebooklm/add-source.test.js +0 -97
- package/clis/notebooklm/compat.test.js +0 -16
- package/clis/notebooklm/create.test.js +0 -58
- package/clis/notebooklm/generate-audio.test.js +0 -63
- package/clis/notebooklm/generate-slides.test.js +0 -75
- package/clis/notebooklm/history.test.js +0 -58
- package/clis/notebooklm/note-list.test.js +0 -56
- package/clis/notebooklm/notes-get.test.js +0 -72
- package/clis/notebooklm/open.test.js +0 -63
- package/clis/notebooklm/rpc.test.js +0 -131
- package/clis/notebooklm/source-fulltext.test.js +0 -106
- package/clis/notebooklm/source-get.test.js +0 -84
- package/clis/notebooklm/source-guide.test.js +0 -104
- package/clis/notebooklm/summary.test.js +0 -78
- package/clis/notebooklm/utils.test.js +0 -449
- package/clis/notebooklm/write-note.test.js +0 -70
- package/clis/nuget/nuget.test.js +0 -111
- package/clis/oeis/oeis.test.js +0 -88
- package/clis/ones/task-helpers.test.js +0 -12
- package/clis/ones/worklog.test.js +0 -20
- package/clis/openfda/openfda.test.js +0 -114
- package/clis/openreview/openreview.test.js +0 -427
- package/clis/osv/osv.test.js +0 -97
- package/clis/paperreview/commands.test.js +0 -243
- package/clis/paperreview/utils.test.js +0 -49
- package/clis/pixiv/download.test.js +0 -87
- package/clis/pixiv/illusts.test.js +0 -99
- package/clis/pixiv/search.test.js +0 -83
- package/clis/powerchina/search.test.js +0 -93
- package/clis/producthunt/utils.test.js +0 -64
- package/clis/pubmed/pubmed.test.js +0 -276
- package/clis/quark/utils.test.js +0 -58
- package/clis/qwen/utils.test.js +0 -45
- package/clis/reddit/extract-media.test.js +0 -149
- package/clis/reddit/frontpage.test.js +0 -34
- package/clis/reddit/home.test.js +0 -170
- package/clis/reddit/hot.test.js +0 -32
- package/clis/reddit/popular.test.js +0 -26
- package/clis/reddit/read.test.js +0 -335
- package/clis/reddit/reply.test.js +0 -89
- package/clis/reddit/search.test.js +0 -26
- package/clis/reddit/subreddit-info.test.js +0 -163
- package/clis/reddit/subreddit.test.js +0 -31
- package/clis/reddit/subscribed.test.js +0 -168
- package/clis/reddit/whoami.test.js +0 -105
- package/clis/rednote/navigation.test.js +0 -26
- package/clis/rednote/rednote.test.js +0 -222
- package/clis/rest-countries/rest-countries.test.js +0 -83
- package/clis/reuters/reuters.test.js +0 -299
- package/clis/rfc/rfc.test.js +0 -74
- package/clis/sinafinance/stock.test.js +0 -59
- package/clis/spotify/utils.test.js +0 -67
- package/clis/stackoverflow/stackoverflow.test.js +0 -404
- package/clis/steam/steam.test.js +0 -46
- package/clis/substack/utils.test.js +0 -44
- package/clis/suno/commands.test.js +0 -188
- package/clis/suno/download.test.js +0 -151
- package/clis/suno/generate.test.js +0 -252
- package/clis/suno/utils.test.js +0 -329
- package/clis/taobao/commands.test.js +0 -41
- package/clis/tdx/hot-rank.test.js +0 -59
- package/clis/ths/hot-rank.test.js +0 -64
- package/clis/tieba/commands.test.js +0 -79
- package/clis/tieba/utils.test.js +0 -290
- package/clis/tiktok/creator-videos.test.js +0 -113
- package/clis/tiktok/refactor.test.js +0 -389
- package/clis/tiktok/write-refactor.test.js +0 -370
- package/clis/toutiao/toutiao.test.js +0 -378
- package/clis/tvmaze/tvmaze.test.js +0 -93
- package/clis/twitter/bookmark-folder.test.js +0 -395
- package/clis/twitter/bookmark-folders.test.js +0 -152
- package/clis/twitter/bookmark.test.js +0 -74
- package/clis/twitter/bookmarks.test.js +0 -205
- package/clis/twitter/delete.test.js +0 -85
- package/clis/twitter/device-follow.test.js +0 -287
- package/clis/twitter/download.test.js +0 -457
- package/clis/twitter/followers.test.js +0 -62
- package/clis/twitter/following.test.js +0 -338
- package/clis/twitter/hide-reply.test.js +0 -76
- package/clis/twitter/like.test.js +0 -73
- package/clis/twitter/likes.test.js +0 -220
- package/clis/twitter/list-add.test.js +0 -133
- package/clis/twitter/list-create.test.js +0 -169
- package/clis/twitter/list-remove.test.js +0 -110
- package/clis/twitter/list-tweets.test.js +0 -196
- package/clis/twitter/lists.test.js +0 -253
- package/clis/twitter/post.test.js +0 -258
- package/clis/twitter/profile.test.js +0 -39
- package/clis/twitter/quote.test.js +0 -194
- package/clis/twitter/reply.test.js +0 -237
- package/clis/twitter/retweet.test.js +0 -73
- package/clis/twitter/search.test.js +0 -349
- package/clis/twitter/shared.test.js +0 -794
- package/clis/twitter/thread.test.js +0 -58
- package/clis/twitter/timeline.test.js +0 -104
- package/clis/twitter/trending.test.js +0 -15
- package/clis/twitter/tweets.test.js +0 -362
- package/clis/twitter/unbookmark.test.js +0 -73
- package/clis/twitter/unlike.test.js +0 -75
- package/clis/twitter/unretweet.test.js +0 -73
- package/clis/twitter/utils.test.js +0 -169
- package/clis/uisdc/news.test.js +0 -66
- package/clis/uiverse/_shared.test.js +0 -62
- package/clis/uiverse/navigation.test.js +0 -12
- package/clis/upwork/upwork.test.js +0 -566
- package/clis/wanfang/search.test.js +0 -23
- package/clis/web/read.test.js +0 -392
- package/clis/weibo/delete.test.js +0 -94
- package/clis/weibo/envelope.test.js +0 -85
- package/clis/weibo/favorites.test.js +0 -114
- package/clis/weibo/publish.test.js +0 -192
- package/clis/weibo/user-posts.test.js +0 -92
- package/clis/weibo/utils.test.js +0 -36
- package/clis/weixin/drafts.test.js +0 -69
- package/clis/weixin/search.test.js +0 -227
- package/clis/weread/ai-outline.test.js +0 -83
- package/clis/weread/book-search.test.js +0 -242
- package/clis/weread/commands.test.js +0 -398
- package/clis/weread/private-api-regression.test.js +0 -369
- package/clis/weread/search-regression.test.js +0 -494
- package/clis/weread/utils.test.js +0 -128
- package/clis/weread-official/commands.test.js +0 -385
- package/clis/weread-official/utils.test.js +0 -242
- package/clis/wikidata/wikidata.test.js +0 -83
- package/clis/wikipedia/trending.test.js +0 -57
- package/clis/wttr/wttr.test.js +0 -84
- package/clis/xianyu/chat.test.js +0 -84
- package/clis/xianyu/im.test.js +0 -253
- package/clis/xianyu/item.test.js +0 -56
- package/clis/xianyu/publish.test.js +0 -220
- package/clis/xianyu/search.test.js +0 -17
- package/clis/xiaoe/xiaoe.test.js +0 -486
- package/clis/xiaohongshu/comments.test.js +0 -258
- package/clis/xiaohongshu/creator-note-detail.test.js +0 -451
- package/clis/xiaohongshu/creator-notes-summary.test.js +0 -94
- package/clis/xiaohongshu/creator-notes.test.js +0 -281
- package/clis/xiaohongshu/creator-stats.test.js +0 -24
- package/clis/xiaohongshu/delete-note.test.js +0 -172
- package/clis/xiaohongshu/download.test.js +0 -316
- package/clis/xiaohongshu/navigation.test.js +0 -34
- package/clis/xiaohongshu/note.test.js +0 -249
- package/clis/xiaohongshu/publish.test.js +0 -608
- package/clis/xiaohongshu/search.test.js +0 -325
- package/clis/xiaohongshu/user-helpers.test.js +0 -160
- package/clis/xiaoyuzhou/auth.test.js +0 -124
- package/clis/xiaoyuzhou/download.test.js +0 -135
- package/clis/xiaoyuzhou/podcast-episodes.test.js +0 -78
- package/clis/xiaoyuzhou/transcript.test.js +0 -195
- package/clis/xiaoyuzhou/utils.test.js +0 -39
- package/clis/xueqiu/comments.test.js +0 -696
- package/clis/xueqiu/danjuan-utils.test.js +0 -41
- package/clis/xueqiu/utils.test.js +0 -26
- package/clis/yahoo/search.test.js +0 -94
- package/clis/youdao/note.test.js +0 -99
- package/clis/youtube/channel.test.js +0 -59
- package/clis/youtube/feed.test.js +0 -131
- package/clis/youtube/transcript-group.test.js +0 -99
- package/clis/youtube/transcript.test.js +0 -296
- package/clis/youtube/utils.test.js +0 -84
- package/clis/yuanbao/ask.test.js +0 -124
- package/clis/yuanbao/new.test.js +0 -30
- package/clis/yuanbao/shared.test.js +0 -80
- package/clis/zhihu/answer-comments.test.js +0 -287
- package/clis/zhihu/answer-detail.test.js +0 -338
- package/clis/zhihu/answer.test.js +0 -54
- package/clis/zhihu/collection.test.js +0 -337
- package/clis/zhihu/collections.test.js +0 -182
- package/clis/zhihu/comment.test.js +0 -50
- package/clis/zhihu/download.test.js +0 -12
- package/clis/zhihu/favorite.test.js +0 -59
- package/clis/zhihu/follow.test.js +0 -42
- package/clis/zhihu/like.test.js +0 -41
- package/clis/zhihu/question.test.js +0 -251
- package/clis/zhihu/recommend.test.js +0 -143
- package/clis/zhihu/search.test.js +0 -198
- package/clis/zhihu/target.test.js +0 -77
- package/clis/zhihu/text.test.js +0 -24
- package/clis/zhihu/write-shared.test.js +0 -176
- package/clis/zlibrary/commands.test.js +0 -65
- package/clis/zsxq/search.test.js +0 -24
- package/clis/zsxq/topic.test.js +0 -30
- package/clis/zsxq/topics.test.js +0 -24
- package/clis/zsxq/utils.test.js +0 -31
- package/dist/src/adapter-shadow.test.js +0 -49
- package/dist/src/adapter-source.test.js +0 -29
- package/dist/src/browser/analyze.test.js +0 -166
- package/dist/src/browser/article-extract.e2e.test.js +0 -105
- package/dist/src/browser/article-extract.test.js +0 -94
- package/dist/src/browser/ax-snapshot.test.js +0 -91
- package/dist/src/browser/base-page.test.js +0 -758
- package/dist/src/browser/cdp-click-fixture.test.js +0 -87
- package/dist/src/browser/cdp.test.js +0 -79
- package/dist/src/browser/compound.test.js +0 -175
- package/dist/src/browser/daemon-client.test.js +0 -199
- package/dist/src/browser/dom-helpers.test.js +0 -106
- package/dist/src/browser/dom-snapshot.test.js +0 -313
- package/dist/src/browser/errors.test.js +0 -60
- package/dist/src/browser/extract.test.js +0 -129
- package/dist/src/browser/find.test.js +0 -180
- package/dist/src/browser/html-tree.test.js +0 -181
- package/dist/src/browser/network-cache.test.js +0 -58
- package/dist/src/browser/network-key.test.js +0 -49
- package/dist/src/browser/page.test.js +0 -378
- package/dist/src/browser/shape-filter.test.js +0 -101
- package/dist/src/browser/shape.test.js +0 -82
- package/dist/src/browser/stealth.test.js +0 -134
- package/dist/src/browser/target-errors.test.js +0 -94
- package/dist/src/browser/target-resolver.test.js +0 -118
- package/dist/src/browser/utils.test.js +0 -29
- package/dist/src/browser/verify-fixture.test.js +0 -219
- package/dist/src/browser.test.js +0 -232
- package/dist/src/build-manifest.test.js +0 -341
- package/dist/src/capabilityRouting.test.js +0 -63
- package/dist/src/cli-argv-preprocess.test.js +0 -209
- package/dist/src/cli.test.js +0 -2803
- package/dist/src/commanderAdapter.test.js +0 -352
- package/dist/src/commands/daemon.test.js +0 -219
- package/dist/src/completion.test.js +0 -24
- package/dist/src/convention-audit.test.js +0 -247
- package/dist/src/daemon.test.js +0 -60
- package/dist/src/doctor.test.js +0 -263
- package/dist/src/download/article-download.test.js +0 -235
- package/dist/src/download/index.test.js +0 -140
- package/dist/src/download/media-download.test.js +0 -112
- package/dist/src/download/progress.test.js +0 -36
- package/dist/src/electron-apps.test.js +0 -49
- package/dist/src/engine.test.js +0 -312
- package/dist/src/errors.test.js +0 -109
- package/dist/src/execution.test.js +0 -623
- package/dist/src/extension-manifest-regression.test.js +0 -12
- package/dist/src/external.test.js +0 -146
- package/dist/src/help.test.js +0 -58
- package/dist/src/hooks.test.js +0 -92
- package/dist/src/interceptor.test.js +0 -81
- package/dist/src/launcher.test.js +0 -138
- package/dist/src/node-network.test.js +0 -55
- package/dist/src/observation/artifact.test.js +0 -121
- package/dist/src/observation/manager.test.js +0 -13
- package/dist/src/observation/redaction.test.js +0 -32
- package/dist/src/observation/retention.test.js +0 -118
- package/dist/src/observation/ring-buffer.test.js +0 -22
- package/dist/src/output.test.js +0 -53
- package/dist/src/package-exports.test.js +0 -112
- package/dist/src/pipeline/executor.test.js +0 -182
- package/dist/src/pipeline/steps/download.test.js +0 -102
- package/dist/src/pipeline/steps/fetch.test.js +0 -117
- package/dist/src/pipeline/template.test.js +0 -175
- package/dist/src/pipeline/transform.test.js +0 -133
- package/dist/src/plugin-manifest.test.js +0 -179
- package/dist/src/plugin-scaffold.test.js +0 -83
- package/dist/src/plugin.test.js +0 -1324
- package/dist/src/registry.test.js +0 -209
- package/dist/src/runtime-detect.test.js +0 -39
- package/dist/src/serialization.test.js +0 -78
- package/dist/src/snapshotFormatter.test.js +0 -521
- package/dist/src/tui.test.js +0 -19
- package/dist/src/update-check.test.js +0 -116
- package/dist/src/utils.test.js +0 -155
- package/dist/src/validate.test.js +0 -90
- package/dist/src/weixin-download.test.js +0 -45
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { downloadArticle } from './article-download.js';
|
|
6
|
-
const tempDirs = [];
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
for (const dir of tempDirs) {
|
|
9
|
-
try {
|
|
10
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
// Ignore cleanup errors in tests.
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
tempDirs.length = 0;
|
|
17
|
-
});
|
|
18
|
-
async function runAndRead(contentHtml, opts = {}) {
|
|
19
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-article-'));
|
|
20
|
-
tempDirs.push(tempDir);
|
|
21
|
-
const result = await downloadArticle({
|
|
22
|
-
title: 'Test Article',
|
|
23
|
-
contentHtml,
|
|
24
|
-
}, {
|
|
25
|
-
output: tempDir,
|
|
26
|
-
downloadImages: false,
|
|
27
|
-
...(opts.cleanSelectors && { cleanSelectors: opts.cleanSelectors }),
|
|
28
|
-
});
|
|
29
|
-
expect(result[0].status).toBe('success');
|
|
30
|
-
return fs.readFileSync(result[0].saved, 'utf8');
|
|
31
|
-
}
|
|
32
|
-
describe('downloadArticle', () => {
|
|
33
|
-
it('returns the saved markdown file path on success', async () => {
|
|
34
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-article-'));
|
|
35
|
-
tempDirs.push(tempDir);
|
|
36
|
-
const result = await downloadArticle({
|
|
37
|
-
title: 'Test Article',
|
|
38
|
-
author: 'Author',
|
|
39
|
-
publishTime: '2026-04-20 12:00:00',
|
|
40
|
-
sourceUrl: 'https://example.com/article',
|
|
41
|
-
contentHtml: '<p>Hello world</p>',
|
|
42
|
-
}, {
|
|
43
|
-
output: tempDir,
|
|
44
|
-
downloadImages: false,
|
|
45
|
-
});
|
|
46
|
-
expect(result).toHaveLength(1);
|
|
47
|
-
expect(result[0].status).toBe('success');
|
|
48
|
-
expect(result[0].saved).toMatch(new RegExp(`^${tempDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
49
|
-
expect(path.extname(result[0].saved)).toBe('.md');
|
|
50
|
-
expect(fs.existsSync(result[0].saved)).toBe(true);
|
|
51
|
-
expect(fs.readFileSync(result[0].saved, 'utf8')).toContain('Hello world');
|
|
52
|
-
});
|
|
53
|
-
describe('markdown pipeline', () => {
|
|
54
|
-
it('converts GFM tables', async () => {
|
|
55
|
-
const md = await runAndRead('<table><thead><tr><th>a</th><th>b</th></tr></thead>' +
|
|
56
|
-
'<tbody><tr><td>1</td><td>2</td></tr></tbody></table>');
|
|
57
|
-
expect(md).toMatch(/\|\s*a\s*\|\s*b\s*\|/);
|
|
58
|
-
expect(md).toMatch(/\|\s*---\s*\|\s*---\s*\|/);
|
|
59
|
-
expect(md).toMatch(/\|\s*1\s*\|\s*2\s*\|/);
|
|
60
|
-
});
|
|
61
|
-
it('converts strikethrough and task lists', async () => {
|
|
62
|
-
const md = await runAndRead('<p><del>gone</del></p>' +
|
|
63
|
-
'<ul><li><input type="checkbox" checked>done</li><li><input type="checkbox">todo</li></ul>');
|
|
64
|
-
expect(md).toContain('~~gone~~');
|
|
65
|
-
expect(md).toContain('[x] done');
|
|
66
|
-
expect(md).toContain('[ ] todo');
|
|
67
|
-
});
|
|
68
|
-
it('strips script / style / noscript / form but keeps iframe as a link', async () => {
|
|
69
|
-
const md = await runAndRead('<p>keep</p>' +
|
|
70
|
-
'<script>alert(1)</script>' +
|
|
71
|
-
'<style>.x{color:red}</style>' +
|
|
72
|
-
'<noscript>nojs</noscript>' +
|
|
73
|
-
'<iframe src="https://www.youtube.com/embed/abc" title="Demo video"></iframe>' +
|
|
74
|
-
'<form><button>click</button></form>');
|
|
75
|
-
expect(md).toContain('keep');
|
|
76
|
-
expect(md).not.toContain('alert');
|
|
77
|
-
expect(md).not.toContain('color:red');
|
|
78
|
-
expect(md).not.toContain('nojs');
|
|
79
|
-
expect(md).not.toContain('click');
|
|
80
|
-
// Iframe degrades to a link preserving the embedded URL.
|
|
81
|
-
expect(md).toContain('[Demo video](https://www.youtube.com/embed/abc)');
|
|
82
|
-
});
|
|
83
|
-
it('strips SVG nodes entirely', async () => {
|
|
84
|
-
const md = await runAndRead('<p>before</p><svg><circle cx="5" cy="5" r="4"/></svg><p>after</p>');
|
|
85
|
-
expect(md).toContain('before');
|
|
86
|
-
expect(md).toContain('after');
|
|
87
|
-
expect(md).not.toContain('svg');
|
|
88
|
-
expect(md).not.toContain('circle');
|
|
89
|
-
});
|
|
90
|
-
it('drops base64 data URI images but keeps regular images', async () => {
|
|
91
|
-
const md = await runAndRead('<p><img alt="inline" src="data:image/png;base64,iVBORw0KGgo="></p>' +
|
|
92
|
-
'<p><img alt="keep" src="https://example.com/a.jpg"></p>');
|
|
93
|
-
expect(md).not.toContain('data:image');
|
|
94
|
-
expect(md).toContain('');
|
|
95
|
-
});
|
|
96
|
-
it('collapses 3+ blank lines and strips lone bullet / middle-dot residue', async () => {
|
|
97
|
-
const md = await runAndRead('<p>top</p>' +
|
|
98
|
-
'<p>-</p>' +
|
|
99
|
-
'<p>·</p>' +
|
|
100
|
-
'<p>bottom</p>');
|
|
101
|
-
expect(md).not.toMatch(/\n{3,}/);
|
|
102
|
-
expect(md).not.toMatch(/^\s*-\s*$/m);
|
|
103
|
-
expect(md).not.toMatch(/^\s*·\s*$/m);
|
|
104
|
-
expect(md).toContain('top');
|
|
105
|
-
expect(md).toContain('bottom');
|
|
106
|
-
});
|
|
107
|
-
it('strips page chrome (header / footer / nav / aside)', async () => {
|
|
108
|
-
const md = await runAndRead('<header><p>page-header-text</p></header>' +
|
|
109
|
-
'<nav><a href="/">home-link</a></nav>' +
|
|
110
|
-
'<p>article-body</p>' +
|
|
111
|
-
'<aside><p>sidebar-text</p></aside>' +
|
|
112
|
-
'<footer><p>page-footer-text</p></footer>');
|
|
113
|
-
expect(md).toContain('article-body');
|
|
114
|
-
expect(md).not.toContain('page-header-text');
|
|
115
|
-
expect(md).not.toContain('home-link');
|
|
116
|
-
expect(md).not.toContain('sidebar-text');
|
|
117
|
-
expect(md).not.toContain('page-footer-text');
|
|
118
|
-
});
|
|
119
|
-
it('cleanSelectors removes matching nodes before conversion', async () => {
|
|
120
|
-
const md = await runAndRead('<p>keep-me</p>' +
|
|
121
|
-
'<div class="vote-card">折叠卡</div>' +
|
|
122
|
-
'<section class="reward-panel">赞赏栏</section>' +
|
|
123
|
-
'<p>also-keep</p>', { cleanSelectors: ['.vote-card', '.reward-panel'] });
|
|
124
|
-
expect(md).toContain('keep-me');
|
|
125
|
-
expect(md).toContain('also-keep');
|
|
126
|
-
expect(md).not.toContain('折叠卡');
|
|
127
|
-
expect(md).not.toContain('赞赏栏');
|
|
128
|
-
});
|
|
129
|
-
it('cleanSelectors silently ignores invalid selectors', async () => {
|
|
130
|
-
const md = await runAndRead('<p>survives</p><div class="x">and-this-too</div>', { cleanSelectors: ['!!!not-a-valid-selector', '.missing'] });
|
|
131
|
-
expect(md).toContain('survives');
|
|
132
|
-
expect(md).toContain('and-this-too');
|
|
133
|
-
});
|
|
134
|
-
it('cleanSelectors keeps valid selectors active when one selector is invalid', async () => {
|
|
135
|
-
const md = await runAndRead('<p>keep</p><div class="vote-card">strip-me</div><p>also-keep</p>', { cleanSelectors: ['!!!not-a-valid-selector', '.vote-card'] });
|
|
136
|
-
expect(md).toContain('keep');
|
|
137
|
-
expect(md).toContain('also-keep');
|
|
138
|
-
expect(md).not.toContain('strip-me');
|
|
139
|
-
});
|
|
140
|
-
it('preserves <video> as inline HTML with src + poster', async () => {
|
|
141
|
-
const md = await runAndRead('<p>before</p>' +
|
|
142
|
-
'<video src="https://cdn.example.com/clip.mp4" poster="https://cdn.example.com/poster.jpg"></video>' +
|
|
143
|
-
'<p>after</p>');
|
|
144
|
-
expect(md).toContain('<video src="https://cdn.example.com/clip.mp4" controls poster="https://cdn.example.com/poster.jpg"></video>');
|
|
145
|
-
expect(md).toContain('before');
|
|
146
|
-
expect(md).toContain('after');
|
|
147
|
-
});
|
|
148
|
-
it('falls back to <source> inside <video> when src attribute is absent', async () => {
|
|
149
|
-
const md = await runAndRead('<video><source src="https://cdn.example.com/clip.mp4" type="video/mp4"></video>');
|
|
150
|
-
expect(md).toContain('<video src="https://cdn.example.com/clip.mp4" controls></video>');
|
|
151
|
-
});
|
|
152
|
-
it('drops <video> with no src and no <source>', async () => {
|
|
153
|
-
const md = await runAndRead('<p>before</p><video></video><p>after</p>');
|
|
154
|
-
expect(md).not.toContain('<video');
|
|
155
|
-
expect(md).toContain('before');
|
|
156
|
-
expect(md).toContain('after');
|
|
157
|
-
});
|
|
158
|
-
it('preserves <audio> as inline HTML', async () => {
|
|
159
|
-
const md = await runAndRead('<audio src="https://cdn.example.com/podcast.mp3"></audio>');
|
|
160
|
-
expect(md).toContain('<audio src="https://cdn.example.com/podcast.mp3" controls></audio>');
|
|
161
|
-
});
|
|
162
|
-
it('degrades <iframe> to a markdown link with title', async () => {
|
|
163
|
-
const md = await runAndRead('<iframe src="https://codepen.io/pen/abc" title="Live demo"></iframe>');
|
|
164
|
-
expect(md).toContain('[Live demo](https://codepen.io/pen/abc)');
|
|
165
|
-
});
|
|
166
|
-
it('defaults iframe title to "Embedded content" when missing', async () => {
|
|
167
|
-
const md = await runAndRead('<iframe src="https://example.com/embed"></iframe>');
|
|
168
|
-
expect(md).toContain('[Embedded content](https://example.com/embed)');
|
|
169
|
-
});
|
|
170
|
-
it('drops <iframe> with no src', async () => {
|
|
171
|
-
const md = await runAndRead('<p>before</p><iframe></iframe><p>after</p>');
|
|
172
|
-
expect(md).not.toContain('iframe');
|
|
173
|
-
expect(md).toContain('before');
|
|
174
|
-
expect(md).toContain('after');
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
describe('stdout mode', () => {
|
|
178
|
-
it('writes markdown to process.stdout and skips file write', async () => {
|
|
179
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-article-'));
|
|
180
|
-
tempDirs.push(tempDir);
|
|
181
|
-
const chunks = [];
|
|
182
|
-
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
183
|
-
process.stdout.write = ((chunk) => {
|
|
184
|
-
chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
|
|
185
|
-
return true;
|
|
186
|
-
});
|
|
187
|
-
try {
|
|
188
|
-
const result = await downloadArticle({
|
|
189
|
-
title: 'Piped',
|
|
190
|
-
contentHtml: '<p>Streaming body</p>',
|
|
191
|
-
sourceUrl: 'https://example.com/a',
|
|
192
|
-
}, {
|
|
193
|
-
output: tempDir,
|
|
194
|
-
stdout: true,
|
|
195
|
-
});
|
|
196
|
-
expect(result[0].status).toBe('success');
|
|
197
|
-
expect(result[0].saved).toBe('-');
|
|
198
|
-
expect(fs.readdirSync(tempDir)).toHaveLength(0);
|
|
199
|
-
const emitted = chunks.join('');
|
|
200
|
-
expect(emitted).toContain('# Piped');
|
|
201
|
-
expect(emitted).toContain('Streaming body');
|
|
202
|
-
expect(emitted.endsWith('\n')).toBe(true);
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
process.stdout.write = originalWrite;
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
it('keeps remote image URLs intact in stdout mode (no download)', async () => {
|
|
209
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-article-'));
|
|
210
|
-
tempDirs.push(tempDir);
|
|
211
|
-
const chunks = [];
|
|
212
|
-
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
213
|
-
process.stdout.write = ((chunk) => {
|
|
214
|
-
chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
|
|
215
|
-
return true;
|
|
216
|
-
});
|
|
217
|
-
try {
|
|
218
|
-
await downloadArticle({
|
|
219
|
-
title: 'WithImage',
|
|
220
|
-
contentHtml: '<p><img src="https://example.com/a.jpg"></p>',
|
|
221
|
-
imageUrls: ['https://example.com/a.jpg'],
|
|
222
|
-
}, {
|
|
223
|
-
output: tempDir,
|
|
224
|
-
downloadImages: true,
|
|
225
|
-
stdout: true,
|
|
226
|
-
});
|
|
227
|
-
expect(fs.readdirSync(tempDir)).toHaveLength(0);
|
|
228
|
-
expect(chunks.join('')).toContain('https://example.com/a.jpg');
|
|
229
|
-
}
|
|
230
|
-
finally {
|
|
231
|
-
process.stdout.write = originalWrite;
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as http from 'node:http';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
|
-
import * as path from 'node:path';
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
import { exportCookiesToNetscape, formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
|
|
7
|
-
const servers = [];
|
|
8
|
-
const tempDirs = [];
|
|
9
|
-
afterEach(async () => {
|
|
10
|
-
vi.unstubAllEnvs();
|
|
11
|
-
await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
|
|
12
|
-
server.close((err) => (err ? reject(err) : resolve()));
|
|
13
|
-
})));
|
|
14
|
-
servers.length = 0;
|
|
15
|
-
for (const dir of tempDirs) {
|
|
16
|
-
try {
|
|
17
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
18
|
-
}
|
|
19
|
-
catch { /* ignore */ }
|
|
20
|
-
}
|
|
21
|
-
tempDirs.length = 0;
|
|
22
|
-
});
|
|
23
|
-
async function startServer(handler, hostname = '127.0.0.1') {
|
|
24
|
-
const server = http.createServer(handler);
|
|
25
|
-
servers.push(server);
|
|
26
|
-
await new Promise((resolve) => server.listen(0, hostname, resolve));
|
|
27
|
-
const address = server.address();
|
|
28
|
-
if (!address || typeof address === 'string') {
|
|
29
|
-
throw new Error('Failed to start test server');
|
|
30
|
-
}
|
|
31
|
-
return `http://${hostname}:${address.port}`;
|
|
32
|
-
}
|
|
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 }, () => {
|
|
36
|
-
it('resolves relative redirects against the original URL', () => {
|
|
37
|
-
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
38
|
-
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
39
|
-
});
|
|
40
|
-
it('formats browser cookies into a Cookie header', () => {
|
|
41
|
-
expect(formatCookieHeader([
|
|
42
|
-
{ name: 'sid', value: 'abc', domain: 'example.com' },
|
|
43
|
-
{ name: 'ct0', value: 'def', domain: 'example.com' },
|
|
44
|
-
])).toBe('sid=abc; ct0=def');
|
|
45
|
-
});
|
|
46
|
-
it('fails after exceeding the redirect limit', async () => {
|
|
47
|
-
const baseUrl = await startServer((_req, res) => {
|
|
48
|
-
res.statusCode = 302;
|
|
49
|
-
res.setHeader('Location', '/loop');
|
|
50
|
-
res.end();
|
|
51
|
-
});
|
|
52
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-dl-'));
|
|
53
|
-
tempDirs.push(tempDir);
|
|
54
|
-
const destPath = path.join(tempDir, 'file.txt');
|
|
55
|
-
const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
|
|
56
|
-
expect(result).toEqual({
|
|
57
|
-
success: false,
|
|
58
|
-
size: 0,
|
|
59
|
-
error: 'Too many redirects (> 2)',
|
|
60
|
-
});
|
|
61
|
-
expect(fs.existsSync(destPath)).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
it('does not forward cookies across cross-domain redirects', async () => {
|
|
64
|
-
let forwardedCookie;
|
|
65
|
-
const targetUrl = await startServer((req, res) => {
|
|
66
|
-
forwardedCookie = req.headers.cookie;
|
|
67
|
-
res.statusCode = 200;
|
|
68
|
-
res.end('ok');
|
|
69
|
-
}, 'localhost');
|
|
70
|
-
const redirectUrl = await startServer((_req, res) => {
|
|
71
|
-
res.statusCode = 302;
|
|
72
|
-
res.setHeader('Location', targetUrl);
|
|
73
|
-
res.end();
|
|
74
|
-
});
|
|
75
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-dl-'));
|
|
76
|
-
tempDirs.push(tempDir);
|
|
77
|
-
const destPath = path.join(tempDir, 'redirect.txt');
|
|
78
|
-
const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
|
|
79
|
-
expect(result).toEqual({ success: true, size: 2 });
|
|
80
|
-
expect(forwardedCookie).toBeUndefined();
|
|
81
|
-
expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
|
|
82
|
-
});
|
|
83
|
-
it('does not forward cookie headers across cross-domain redirects', async () => {
|
|
84
|
-
let forwardedCookie;
|
|
85
|
-
const targetUrl = await startServer((req, res) => {
|
|
86
|
-
forwardedCookie = req.headers.cookie;
|
|
87
|
-
res.statusCode = 200;
|
|
88
|
-
res.end('ok');
|
|
89
|
-
}, 'localhost');
|
|
90
|
-
const redirectUrl = await startServer((_req, res) => {
|
|
91
|
-
res.statusCode = 302;
|
|
92
|
-
res.setHeader('Location', targetUrl);
|
|
93
|
-
res.end();
|
|
94
|
-
});
|
|
95
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-dl-'));
|
|
96
|
-
tempDirs.push(tempDir);
|
|
97
|
-
const destPath = path.join(tempDir, 'redirect-header.txt');
|
|
98
|
-
const result = await httpDownload(`${redirectUrl}/start`, destPath, {
|
|
99
|
-
headers: { Cookie: 'sid=header-cookie' },
|
|
100
|
-
});
|
|
101
|
-
expect(result).toEqual({ success: true, size: 2 });
|
|
102
|
-
expect(forwardedCookie).toBeUndefined();
|
|
103
|
-
expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
|
|
104
|
-
});
|
|
105
|
-
it('bypasses proxy settings for loopback downloads', async () => {
|
|
106
|
-
vi.stubEnv('HTTP_PROXY', 'http://127.0.0.1:9');
|
|
107
|
-
const baseUrl = await startServer((_req, res) => {
|
|
108
|
-
res.statusCode = 200;
|
|
109
|
-
res.end('ok');
|
|
110
|
-
});
|
|
111
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-dl-'));
|
|
112
|
-
tempDirs.push(tempDir);
|
|
113
|
-
const destPath = path.join(tempDir, 'loopback.txt');
|
|
114
|
-
const result = await httpDownload(`${baseUrl}/ok`, destPath);
|
|
115
|
-
expect(result).toEqual({ success: true, size: 2 });
|
|
116
|
-
expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
|
|
117
|
-
});
|
|
118
|
-
it.skipIf(process.platform === 'win32')('writes the Netscape cookie file with 0o600 owner-only permissions', async () => {
|
|
119
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-dl-'));
|
|
120
|
-
tempDirs.push(tempDir);
|
|
121
|
-
const cookiesPath = path.join(tempDir, 'cookies.txt');
|
|
122
|
-
exportCookiesToNetscape([
|
|
123
|
-
{ name: 'sid', value: 'secret', domain: 'example.com' },
|
|
124
|
-
], cookiesPath);
|
|
125
|
-
const mode = fs.statSync(cookiesPath).mode & 0o777;
|
|
126
|
-
expect(mode).toBe(0o600);
|
|
127
|
-
});
|
|
128
|
-
it.skipIf(process.platform === 'win32')('tightens an existing Netscape cookie file before rewriting it', async () => {
|
|
129
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-dl-'));
|
|
130
|
-
tempDirs.push(tempDir);
|
|
131
|
-
const cookiesPath = path.join(tempDir, 'cookies.txt');
|
|
132
|
-
fs.writeFileSync(cookiesPath, 'stale-cookie', { mode: 0o644 });
|
|
133
|
-
exportCookiesToNetscape([
|
|
134
|
-
{ name: 'sid', value: 'secret', domain: 'example.com' },
|
|
135
|
-
], cookiesPath);
|
|
136
|
-
const mode = fs.statSync(cookiesPath).mode & 0o777;
|
|
137
|
-
expect(mode).toBe(0o600);
|
|
138
|
-
expect(fs.readFileSync(cookiesPath, 'utf8')).toContain('sid\tsecret');
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as http from 'node:http';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
|
-
import * as path from 'node:path';
|
|
5
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import { downloadMedia } from './media-download.js';
|
|
7
|
-
const servers = [];
|
|
8
|
-
const tempDirs = [];
|
|
9
|
-
afterEach(async () => {
|
|
10
|
-
await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
|
|
11
|
-
server.close((err) => (err ? reject(err) : resolve()));
|
|
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;
|
|
21
|
-
});
|
|
22
|
-
async function startServer(handler) {
|
|
23
|
-
const server = http.createServer(handler);
|
|
24
|
-
servers.push(server);
|
|
25
|
-
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
26
|
-
const address = server.address();
|
|
27
|
-
if (!address || typeof address === 'string') {
|
|
28
|
-
throw new Error('Failed to start test server');
|
|
29
|
-
}
|
|
30
|
-
return `http://127.0.0.1:${address.port}`;
|
|
31
|
-
}
|
|
32
|
-
// Windows runners occasionally exceed the default 5s timeout on the first
|
|
33
|
-
// http.createServer + downloadMedia roundtrip (cold-start cost on a loaded VM).
|
|
34
|
-
describe('media downloads', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
|
|
35
|
-
it('keeps custom filenames inside the output directory', async () => {
|
|
36
|
-
const baseUrl = await startServer((_req, res) => {
|
|
37
|
-
res.statusCode = 200;
|
|
38
|
-
res.end('image');
|
|
39
|
-
});
|
|
40
|
-
const parentDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-media-parent-'));
|
|
41
|
-
tempDirs.push(parentDir);
|
|
42
|
-
const outputDir = path.join(parentDir, 'downloads');
|
|
43
|
-
const results = await downloadMedia([
|
|
44
|
-
{ type: 'image', url: `${baseUrl}/image.jpg`, filename: '../escape.jpg' },
|
|
45
|
-
], {
|
|
46
|
-
output: outputDir,
|
|
47
|
-
verbose: false,
|
|
48
|
-
});
|
|
49
|
-
expect(results).toEqual([
|
|
50
|
-
{ index: 1, type: 'image', status: 'success', size: '5.0 B' },
|
|
51
|
-
]);
|
|
52
|
-
expect(fs.readFileSync(path.join(outputDir, 'escape.jpg'), 'utf8')).toBe('image');
|
|
53
|
-
expect(fs.existsSync(path.join(parentDir, 'escape.jpg'))).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
it('strips nested and absolute path components from custom filenames', async () => {
|
|
56
|
-
const baseUrl = await startServer((_req, res) => {
|
|
57
|
-
res.statusCode = 200;
|
|
58
|
-
res.end('image');
|
|
59
|
-
});
|
|
60
|
-
const parentDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-media-parent-'));
|
|
61
|
-
tempDirs.push(parentDir);
|
|
62
|
-
const outputDir = path.join(parentDir, 'downloads');
|
|
63
|
-
const results = await downloadMedia([
|
|
64
|
-
{ type: 'image', url: `${baseUrl}/one.jpg`, filename: '../../etc/passwd' },
|
|
65
|
-
{ type: 'image', url: `${baseUrl}/two.jpg`, filename: '/tmp/evil.jpg' },
|
|
66
|
-
{ type: 'image', url: `${baseUrl}/three.jpg`, filename: 'nested/path/photo.png' },
|
|
67
|
-
{ type: 'image', url: `${baseUrl}/four.jpg`, filename: '..\\windows\\escape.jpg' },
|
|
68
|
-
], {
|
|
69
|
-
output: outputDir,
|
|
70
|
-
verbose: false,
|
|
71
|
-
});
|
|
72
|
-
expect(results).toEqual([
|
|
73
|
-
{ index: 1, type: 'image', status: 'success', size: '5.0 B' },
|
|
74
|
-
{ index: 2, type: 'image', status: 'success', size: '5.0 B' },
|
|
75
|
-
{ index: 3, type: 'image', status: 'success', size: '5.0 B' },
|
|
76
|
-
{ index: 4, type: 'image', status: 'success', size: '5.0 B' },
|
|
77
|
-
]);
|
|
78
|
-
expect(fs.readFileSync(path.join(outputDir, 'passwd'), 'utf8')).toBe('image');
|
|
79
|
-
expect(fs.readFileSync(path.join(outputDir, 'evil.jpg'), 'utf8')).toBe('image');
|
|
80
|
-
expect(fs.readFileSync(path.join(outputDir, 'photo.png'), 'utf8')).toBe('image');
|
|
81
|
-
expect(fs.readFileSync(path.join(outputDir, 'escape.jpg'), 'utf8')).toBe('image');
|
|
82
|
-
expect(fs.existsSync(path.join(parentDir, 'etc', 'passwd'))).toBe(false);
|
|
83
|
-
expect(fs.existsSync(path.join(parentDir, 'tmp', 'evil.jpg'))).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
it('falls back to generated names for empty dot-directory filenames', async () => {
|
|
86
|
-
const baseUrl = await startServer((_req, res) => {
|
|
87
|
-
res.statusCode = 200;
|
|
88
|
-
res.end('image');
|
|
89
|
-
});
|
|
90
|
-
const parentDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'bycli-media-parent-'));
|
|
91
|
-
tempDirs.push(parentDir);
|
|
92
|
-
const outputDir = path.join(parentDir, 'downloads');
|
|
93
|
-
const results = await downloadMedia([
|
|
94
|
-
{ type: 'image', url: `${baseUrl}/dot.jpg`, filename: '.' },
|
|
95
|
-
{ type: 'image', url: `${baseUrl}/dotdot.jpg`, filename: '..' },
|
|
96
|
-
{ type: 'image', url: `${baseUrl}/blank.jpg`, filename: ' ' },
|
|
97
|
-
], {
|
|
98
|
-
output: outputDir,
|
|
99
|
-
filenamePrefix: '../unsafe prefix',
|
|
100
|
-
verbose: false,
|
|
101
|
-
});
|
|
102
|
-
expect(results).toEqual([
|
|
103
|
-
{ index: 1, type: 'image', status: 'success', size: '5.0 B' },
|
|
104
|
-
{ index: 2, type: 'image', status: 'success', size: '5.0 B' },
|
|
105
|
-
{ index: 3, type: 'image', status: 'success', size: '5.0 B' },
|
|
106
|
-
]);
|
|
107
|
-
expect(fs.readFileSync(path.join(outputDir, 'unsafe_prefix_1.jpg'), 'utf8')).toBe('image');
|
|
108
|
-
expect(fs.readFileSync(path.join(outputDir, 'unsafe_prefix_2.jpg'), 'utf8')).toBe('image');
|
|
109
|
-
expect(fs.readFileSync(path.join(outputDir, 'unsafe_prefix_3.jpg'), 'utf8')).toBe('image');
|
|
110
|
-
expect(fs.existsSync(path.join(parentDir, 'unsafe prefix_1.jpg'))).toBe(false);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { createProgressBar, formatBytes } from './progress.js';
|
|
3
|
-
describe('formatBytes', () => {
|
|
4
|
-
it('returns a stable zero-byte label for invalid or sub-byte values', () => {
|
|
5
|
-
expect(formatBytes(-1)).toBe('0 B');
|
|
6
|
-
expect(formatBytes(0.5)).toBe('0 B');
|
|
7
|
-
expect(formatBytes(Number.NaN)).toBe('0 B');
|
|
8
|
-
expect(formatBytes(Number.POSITIVE_INFINITY)).toBe('0 B');
|
|
9
|
-
});
|
|
10
|
-
it('caps very large values at the largest supported unit', () => {
|
|
11
|
-
expect(formatBytes(1024 ** 5)).toBe('1024.0 TB');
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
describe('download progress display', () => {
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
vi.restoreAllMocks();
|
|
17
|
-
});
|
|
18
|
-
it('clamps percentages above 100 to keep the progress bar renderable', () => {
|
|
19
|
-
const write = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
20
|
-
const progress = createProgressBar('file.bin', 0, 1);
|
|
21
|
-
expect(() => progress.update(150, 100)).not.toThrow();
|
|
22
|
-
expect(write).toHaveBeenCalledWith(expect.stringContaining('100%'));
|
|
23
|
-
});
|
|
24
|
-
it('clamps negative percentages to zero', () => {
|
|
25
|
-
const write = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
26
|
-
const progress = createProgressBar('file.bin', 0, 1);
|
|
27
|
-
expect(() => progress.update(-10, 100)).not.toThrow();
|
|
28
|
-
expect(write).toHaveBeenCalledWith(expect.stringContaining('0%'));
|
|
29
|
-
});
|
|
30
|
-
it('renders zero percent when the total size is unknown', () => {
|
|
31
|
-
const write = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
32
|
-
const progress = createProgressBar('file.bin', 0, 1);
|
|
33
|
-
expect(() => progress.update(50, 0)).not.toThrow();
|
|
34
|
-
expect(write).toHaveBeenCalledWith(expect.stringContaining('0%'));
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { builtinApps, getElectronApp, isElectronApp, loadApps } from './electron-apps.js';
|
|
3
|
-
describe('electron-apps registry', () => {
|
|
4
|
-
it('returns builtin app entry for cursor', () => {
|
|
5
|
-
const app = getElectronApp('cursor');
|
|
6
|
-
expect(app).toBeDefined();
|
|
7
|
-
expect(app.port).toBe(9226);
|
|
8
|
-
expect(app.processName).toBe('Cursor');
|
|
9
|
-
});
|
|
10
|
-
it('returns builtin app entry for codex', () => {
|
|
11
|
-
const app = getElectronApp('codex');
|
|
12
|
-
expect(app).toBeDefined();
|
|
13
|
-
expect(app.port).toBe(9238);
|
|
14
|
-
});
|
|
15
|
-
it('keeps builtin Electron app CDP ports unique and off the browser-bridge port', () => {
|
|
16
|
-
const ports = Object.values(builtinApps).map((app) => app.port);
|
|
17
|
-
expect(new Set(ports).size).toBe(ports.length);
|
|
18
|
-
expect(ports).not.toContain(9222);
|
|
19
|
-
});
|
|
20
|
-
it('returns undefined for non-Electron sites', () => {
|
|
21
|
-
expect(getElectronApp('bilibili')).toBeUndefined();
|
|
22
|
-
expect(getElectronApp('hackernews')).toBeUndefined();
|
|
23
|
-
});
|
|
24
|
-
it('isElectronApp returns true for registered apps', () => {
|
|
25
|
-
expect(isElectronApp('cursor')).toBe(true);
|
|
26
|
-
expect(isElectronApp('codex')).toBe(true);
|
|
27
|
-
expect(isElectronApp('chatwise')).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
it('isElectronApp returns false for non-Electron sites', () => {
|
|
30
|
-
expect(isElectronApp('bilibili')).toBe(false);
|
|
31
|
-
expect(isElectronApp('notion')).toBe(false);
|
|
32
|
-
expect(isElectronApp('unknown-app')).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
it('loadApps merges user config additively', () => {
|
|
35
|
-
const apps = loadApps({
|
|
36
|
-
myapp: { port: 9234, processName: 'MyApp' },
|
|
37
|
-
});
|
|
38
|
-
expect(apps.myapp).toBeDefined();
|
|
39
|
-
expect(apps.myapp.port).toBe(9234);
|
|
40
|
-
// Builtins still present
|
|
41
|
-
expect(apps.cursor).toBeDefined();
|
|
42
|
-
});
|
|
43
|
-
it('loadApps does not override builtin entries', () => {
|
|
44
|
-
const apps = loadApps({
|
|
45
|
-
cursor: { port: 9999, processName: 'FakeCursor' },
|
|
46
|
-
});
|
|
47
|
-
expect(apps.cursor.port).toBe(9226); // Builtin wins
|
|
48
|
-
});
|
|
49
|
-
});
|