@jackwener/opencli 1.3.2 → 1.4.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/pull_request_template.md +3 -1
- package/.github/workflows/build-extension.yml +7 -1
- package/.github/workflows/ci.yml +29 -3
- package/.github/workflows/docs.yml +1 -1
- package/.github/workflows/e2e-headed.yml +20 -0
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/security.yml +0 -3
- package/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +6 -3
- package/README.md +37 -10
- package/README.zh-CN.md +37 -10
- package/SKILL.md +7 -2
- package/TESTING.md +1 -0
- package/chatwise-opencli.ps1 +82 -0
- package/dist/analysis.d.ts +38 -0
- package/dist/analysis.js +166 -0
- package/dist/browser/cdp.d.ts +0 -4
- package/dist/browser/cdp.js +59 -38
- package/dist/browser/cdp.test.d.ts +1 -0
- package/dist/browser/cdp.test.js +52 -0
- package/dist/browser/daemon-client.js +2 -1
- package/dist/browser/discover.js +2 -1
- package/dist/browser/dom-snapshot.d.ts +2 -2
- package/dist/browser/dom-snapshot.js +54 -1
- package/dist/browser/dom-snapshot.test.js +36 -0
- package/dist/browser/errors.js +2 -1
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +2 -1
- package/dist/browser/mcp.d.ts +0 -2
- package/dist/browser/mcp.js +2 -3
- package/dist/browser/page.d.ts +4 -3
- package/dist/browser/page.js +44 -35
- package/dist/browser/stealth.d.ts +16 -0
- package/dist/browser/stealth.js +155 -0
- package/dist/browser.test.js +47 -1
- package/dist/build-manifest.js +15 -9
- package/dist/build-manifest.test.js +12 -0
- package/dist/cascade.js +4 -2
- package/dist/cli-manifest.json +639 -258
- package/dist/cli.js +57 -29
- package/dist/clis/_shared/desktop-commands.d.ts +22 -0
- package/dist/clis/_shared/desktop-commands.js +108 -0
- package/dist/clis/antigravity/serve.js +5 -2
- package/dist/clis/arxiv/search.js +1 -1
- package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
- package/dist/clis/bilibili/dynamic.test.js +68 -0
- package/dist/clis/bilibili/favorite.js +4 -2
- package/dist/clis/bilibili/following.js +3 -2
- package/dist/clis/bilibili/subtitle.js +8 -7
- package/dist/clis/bilibili/utils.js +2 -2
- package/dist/clis/boss/batchgreet.js +1 -1
- package/dist/clis/boss/chatlist.js +1 -1
- package/dist/clis/boss/chatmsg.js +1 -1
- package/dist/clis/boss/detail.js +1 -1
- package/dist/clis/boss/exchange.js +1 -1
- package/dist/clis/boss/greet.js +1 -1
- package/dist/clis/boss/invite.js +1 -1
- package/dist/clis/boss/joblist.js +1 -1
- package/dist/clis/boss/mark.js +4 -3
- package/dist/clis/boss/recommend.js +1 -1
- package/dist/clis/boss/resume.js +1 -1
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +5 -4
- package/dist/clis/boss/stats.js +1 -1
- package/dist/clis/chatgpt/ask.js +4 -0
- package/dist/clis/chatgpt/new.js +5 -1
- package/dist/clis/chatgpt/read.js +5 -1
- package/dist/clis/chatgpt/send.js +2 -1
- package/dist/clis/chatgpt/status.js +5 -1
- package/dist/clis/chatwise/ask.js +8 -2
- package/dist/clis/chatwise/export.js +2 -0
- package/dist/clis/chatwise/history.js +2 -0
- package/dist/clis/chatwise/model.js +8 -3
- package/dist/clis/chatwise/new.js +3 -18
- package/dist/clis/chatwise/read.js +2 -0
- package/dist/clis/chatwise/screenshot.js +3 -27
- package/dist/clis/chatwise/send.js +8 -2
- package/dist/clis/chatwise/shared.d.ts +2 -0
- package/dist/clis/chatwise/shared.js +6 -0
- package/dist/clis/chatwise/status.js +3 -22
- package/dist/clis/codex/ask.js +6 -2
- package/dist/clis/codex/dump.js +2 -25
- package/dist/clis/codex/new.js +2 -25
- package/dist/clis/codex/screenshot.js +2 -27
- package/dist/clis/codex/send.js +6 -4
- package/dist/clis/codex/status.js +2 -22
- package/dist/clis/cursor/ask.js +2 -1
- package/dist/clis/cursor/composer.js +2 -1
- package/dist/clis/cursor/dump.js +2 -25
- package/dist/clis/cursor/new.js +2 -18
- package/dist/clis/cursor/read.js +2 -1
- package/dist/clis/cursor/screenshot.js +1 -30
- package/dist/clis/cursor/send.js +2 -1
- package/dist/clis/cursor/status.js +2 -21
- package/dist/clis/dictionary/examples.yaml +25 -0
- package/dist/clis/dictionary/search.yaml +27 -0
- package/dist/clis/dictionary/synonyms.yaml +25 -0
- package/dist/clis/douban/book-hot.js +1 -1
- package/dist/clis/douban/movie-hot.js +1 -1
- package/dist/clis/douban/search.js +1 -1
- package/dist/clis/douban/utils.d.ts +4 -1
- package/dist/clis/douban/utils.js +156 -1
- package/dist/clis/doubao/ask.js +1 -1
- package/dist/clis/doubao/new.js +1 -1
- package/dist/clis/doubao/read.js +1 -1
- package/dist/clis/doubao/send.js +1 -1
- package/dist/clis/doubao/status.js +1 -1
- package/dist/clis/doubao-app/ask.js +1 -1
- package/dist/clis/doubao-app/new.js +1 -1
- package/dist/clis/doubao-app/read.js +1 -1
- package/dist/clis/doubao-app/send.js +1 -1
- package/dist/clis/grok/ask.d.ts +4 -0
- package/dist/clis/grok/ask.js +28 -10
- package/dist/clis/grok/ask.test.js +18 -0
- package/dist/clis/jd/item.d.ts +1 -0
- package/dist/clis/jd/item.js +96 -0
- package/dist/clis/jd/item.test.d.ts +1 -0
- package/dist/clis/jd/item.test.js +28 -0
- package/dist/clis/jike/feed.js +1 -1
- package/dist/clis/jike/search.js +1 -1
- package/dist/clis/linkedin/search.js +5 -4
- package/dist/clis/linkedin/timeline.d.ts +21 -0
- package/dist/clis/linkedin/timeline.js +503 -0
- package/dist/clis/linkedin/timeline.test.d.ts +1 -0
- package/dist/clis/linkedin/timeline.test.js +81 -0
- package/dist/clis/medium/feed.js +1 -1
- package/dist/clis/medium/search.js +1 -1
- package/dist/clis/medium/user.js +1 -1
- package/dist/clis/medium/{shared.js → utils.js} +2 -1
- package/dist/clis/pixiv/detail.yaml +49 -0
- package/dist/clis/pixiv/download.d.ts +7 -0
- package/dist/clis/pixiv/download.js +78 -0
- package/dist/clis/pixiv/download.test.d.ts +1 -0
- package/dist/clis/pixiv/download.test.js +87 -0
- package/dist/clis/pixiv/illusts.d.ts +8 -0
- package/dist/clis/pixiv/illusts.js +65 -0
- package/dist/clis/pixiv/illusts.test.d.ts +1 -0
- package/dist/clis/pixiv/illusts.test.js +99 -0
- package/dist/clis/pixiv/ranking.yaml +53 -0
- package/dist/clis/pixiv/search.d.ts +6 -0
- package/dist/clis/pixiv/search.js +43 -0
- package/dist/clis/pixiv/search.test.d.ts +1 -0
- package/dist/clis/pixiv/search.test.js +83 -0
- package/dist/clis/pixiv/test-utils.d.ts +12 -0
- package/dist/clis/pixiv/test-utils.js +23 -0
- package/dist/clis/pixiv/user.yaml +46 -0
- package/dist/clis/pixiv/utils.d.ts +27 -0
- package/dist/clis/pixiv/utils.js +49 -0
- package/dist/clis/reddit/comment.js +2 -1
- package/dist/clis/reddit/read.js +4 -3
- package/dist/clis/reddit/read.test.d.ts +1 -0
- package/dist/clis/reddit/read.test.js +28 -0
- package/dist/clis/reddit/save.js +2 -1
- package/dist/clis/reddit/saved.js +7 -3
- package/dist/clis/reddit/subscribe.js +2 -1
- package/dist/clis/reddit/upvote.js +2 -1
- package/dist/clis/reddit/upvoted.js +7 -3
- package/dist/clis/sinablog/article.js +1 -1
- package/dist/clis/sinablog/hot.js +1 -1
- package/dist/clis/sinablog/user.js +1 -1
- package/dist/clis/substack/feed.js +1 -1
- package/dist/clis/substack/publication.js +1 -1
- package/dist/clis/substack/search.js +3 -2
- package/dist/clis/substack/{shared.js → utils.js} +3 -2
- package/dist/clis/tiktok/search.yaml +2 -1
- package/dist/clis/twitter/accept.js +2 -1
- package/dist/clis/twitter/article.js +4 -1
- package/dist/clis/twitter/block.js +2 -1
- package/dist/clis/twitter/bookmark.js +2 -1
- package/dist/clis/twitter/bookmarks.js +3 -2
- package/dist/clis/twitter/delete.js +2 -1
- package/dist/clis/twitter/follow.js +2 -1
- package/dist/clis/twitter/followers.js +3 -2
- package/dist/clis/twitter/following.js +3 -2
- package/dist/clis/twitter/hide-reply.js +2 -1
- package/dist/clis/twitter/like.js +2 -1
- package/dist/clis/twitter/notifications.js +2 -1
- package/dist/clis/twitter/post.js +2 -1
- package/dist/clis/twitter/profile.js +5 -2
- package/dist/clis/twitter/reply-dm.js +2 -1
- package/dist/clis/twitter/reply.js +2 -1
- package/dist/clis/twitter/search.js +30 -13
- package/dist/clis/twitter/search.test.d.ts +1 -0
- package/dist/clis/twitter/search.test.js +104 -0
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/timeline.js +3 -2
- package/dist/clis/twitter/trending.js +3 -2
- package/dist/clis/twitter/unblock.js +2 -1
- package/dist/clis/twitter/unbookmark.js +2 -1
- package/dist/clis/twitter/unfollow.js +2 -1
- package/dist/clis/v2ex/daily.js +3 -2
- package/dist/clis/v2ex/me.js +3 -2
- package/dist/clis/v2ex/notifications.js +4 -4
- package/dist/clis/web/read.d.ts +16 -0
- package/dist/clis/web/read.js +202 -0
- package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
- package/dist/clis/xueqiu/danjuan-utils.js +126 -0
- package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
- package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
- package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
- package/dist/clis/xueqiu/fund-holdings.js +28 -0
- package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
- package/dist/clis/xueqiu/fund-snapshot.js +25 -0
- package/dist/clis/youtube/transcript.js +5 -4
- package/dist/clis/youtube/video.js +3 -2
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/daemon.js +9 -4
- package/dist/discovery.js +11 -10
- package/dist/doctor.js +4 -2
- package/dist/download/index.d.ts +4 -12
- package/dist/download/index.js +33 -12
- package/dist/download/index.test.js +79 -2
- package/dist/download/media-download.js +4 -2
- package/dist/engine.test.js +76 -4
- package/dist/execution.d.ts +1 -9
- package/dist/execution.js +56 -46
- package/dist/explore.js +12 -111
- package/dist/external-clis.yaml +0 -8
- package/dist/external.js +7 -5
- package/dist/external.test.js +4 -0
- package/dist/generate.d.ts +0 -9
- package/dist/generate.js +4 -20
- package/dist/hooks.d.ts +46 -0
- package/dist/hooks.js +56 -0
- package/dist/hooks.test.d.ts +4 -0
- package/dist/hooks.test.js +92 -0
- package/dist/interceptor.js +70 -23
- package/dist/main.js +2 -0
- package/dist/output.js +12 -6
- package/dist/pipeline/executor.js +1 -1
- package/dist/pipeline/steps/browser.js +1 -3
- package/dist/pipeline/steps/download.js +42 -26
- package/dist/pipeline/steps/download.test.d.ts +1 -0
- package/dist/pipeline/steps/download.test.js +101 -0
- package/dist/pipeline/steps/fetch.js +40 -22
- package/dist/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/pipeline/steps/fetch.test.js +123 -0
- package/dist/pipeline/steps/transform.js +2 -6
- package/dist/pipeline/template.js +66 -52
- package/dist/pipeline/template.test.js +28 -0
- package/dist/pipeline/transform.test.js +18 -0
- package/dist/plugin.d.ts +40 -1
- package/dist/plugin.js +214 -17
- package/dist/plugin.test.d.ts +1 -1
- package/dist/plugin.test.js +219 -3
- package/dist/record.js +6 -98
- package/dist/registry-api.d.ts +2 -0
- package/dist/registry-api.js +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +1 -2
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +14 -4
- package/dist/snapshotFormatter.d.ts +7 -14
- package/dist/snapshotFormatter.js +38 -78
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +29 -0
- package/dist/validate.js +3 -5
- package/dist/yaml-schema.d.ts +26 -0
- package/dist/yaml-schema.js +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/dictionary.md +27 -0
- package/docs/adapters/browser/douban.md +18 -8
- package/docs/adapters/browser/jd.md +27 -0
- package/docs/adapters/browser/linkedin.md +6 -0
- package/docs/adapters/browser/pixiv.md +92 -0
- package/docs/adapters/browser/web.md +30 -0
- package/docs/adapters/browser/wikipedia.md +0 -9
- package/docs/adapters/browser/xueqiu.md +27 -9
- package/docs/adapters/desktop/antigravity.md +0 -3
- package/docs/adapters/index.md +11 -9
- package/docs/comparison.md +125 -0
- package/docs/developer/contributing.md +21 -2
- package/docs/developer/testing.md +14 -8
- package/docs/developer/ts-adapter.md +18 -0
- package/docs/developer/yaml-adapter.md +16 -0
- package/docs/guide/plugins.md +10 -0
- package/docs/zh/guide/plugins.md +10 -0
- package/extension/dist/background.js +519 -444
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.test.ts +46 -1
- package/extension/src/background.ts +108 -33
- package/extension/src/cdp.ts +9 -9
- package/package.json +3 -2
- package/scripts/check-doc-coverage.sh +2 -0
- package/src/analysis.ts +170 -0
- package/src/browser/cdp.test.ts +66 -0
- package/src/browser/cdp.ts +64 -41
- package/src/browser/daemon-client.ts +4 -3
- package/src/browser/discover.ts +2 -1
- package/src/browser/dom-snapshot.test.ts +42 -0
- package/src/browser/dom-snapshot.ts +56 -3
- package/src/browser/errors.ts +2 -1
- package/src/browser/index.ts +3 -2
- package/src/browser/mcp.ts +2 -4
- package/src/browser/page.ts +43 -35
- package/src/browser/stealth.ts +156 -0
- package/src/browser.test.ts +51 -1
- package/src/build-manifest.test.ts +14 -0
- package/src/build-manifest.ts +13 -32
- package/src/cascade.ts +5 -3
- package/src/cli.ts +66 -34
- package/src/clis/_shared/desktop-commands.ts +121 -0
- package/src/clis/antigravity/serve.ts +6 -3
- package/src/clis/arxiv/search.ts +1 -1
- package/src/clis/bilibili/dynamic.test.ts +79 -0
- package/src/clis/bilibili/favorite.ts +5 -2
- package/src/clis/bilibili/following.ts +3 -2
- package/src/clis/bilibili/subtitle.ts +8 -7
- package/src/clis/bilibili/utils.ts +2 -2
- package/src/clis/boss/batchgreet.ts +1 -1
- package/src/clis/boss/chatlist.ts +1 -1
- package/src/clis/boss/chatmsg.ts +1 -1
- package/src/clis/boss/detail.ts +1 -1
- package/src/clis/boss/exchange.ts +1 -1
- package/src/clis/boss/greet.ts +1 -1
- package/src/clis/boss/invite.ts +1 -1
- package/src/clis/boss/joblist.ts +1 -1
- package/src/clis/boss/mark.ts +4 -3
- package/src/clis/boss/recommend.ts +1 -1
- package/src/clis/boss/resume.ts +1 -1
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +5 -4
- package/src/clis/boss/stats.ts +1 -1
- package/src/clis/chatgpt/ask.ts +5 -0
- package/src/clis/chatgpt/new.ts +7 -2
- package/src/clis/chatgpt/read.ts +7 -2
- package/src/clis/chatgpt/send.ts +3 -2
- package/src/clis/chatgpt/status.ts +6 -1
- package/src/clis/chatwise/ask.ts +7 -2
- package/src/clis/chatwise/export.ts +2 -0
- package/src/clis/chatwise/history.ts +2 -0
- package/src/clis/chatwise/model.ts +7 -3
- package/src/clis/chatwise/new.ts +3 -20
- package/src/clis/chatwise/read.ts +2 -0
- package/src/clis/chatwise/screenshot.ts +3 -32
- package/src/clis/chatwise/send.ts +7 -2
- package/src/clis/chatwise/shared.ts +8 -0
- package/src/clis/chatwise/status.ts +3 -24
- package/src/clis/codex/ask.ts +5 -2
- package/src/clis/codex/dump.ts +2 -27
- package/src/clis/codex/new.ts +2 -28
- package/src/clis/codex/screenshot.ts +2 -32
- package/src/clis/codex/send.ts +5 -4
- package/src/clis/codex/status.ts +2 -24
- package/src/clis/cursor/ask.ts +2 -1
- package/src/clis/cursor/composer.ts +2 -1
- package/src/clis/cursor/dump.ts +2 -27
- package/src/clis/cursor/new.ts +2 -20
- package/src/clis/cursor/read.ts +2 -1
- package/src/clis/cursor/screenshot.ts +1 -36
- package/src/clis/cursor/send.ts +2 -1
- package/src/clis/cursor/status.ts +2 -22
- package/src/clis/dictionary/examples.yaml +25 -0
- package/src/clis/dictionary/search.yaml +27 -0
- package/src/clis/dictionary/synonyms.yaml +25 -0
- package/src/clis/douban/book-hot.ts +1 -1
- package/src/clis/douban/movie-hot.ts +1 -1
- package/src/clis/douban/search.ts +1 -1
- package/src/clis/douban/utils.ts +165 -1
- package/src/clis/doubao/ask.ts +1 -1
- package/src/clis/doubao/new.ts +1 -1
- package/src/clis/doubao/read.ts +1 -1
- package/src/clis/doubao/send.ts +1 -1
- package/src/clis/doubao/status.ts +1 -1
- package/src/clis/doubao-app/ask.ts +1 -1
- package/src/clis/doubao-app/new.ts +1 -1
- package/src/clis/doubao-app/read.ts +1 -1
- package/src/clis/doubao-app/send.ts +1 -1
- package/src/clis/grok/ask.test.ts +25 -0
- package/src/clis/grok/ask.ts +25 -12
- package/src/clis/jd/item.test.ts +35 -0
- package/src/clis/jd/item.ts +101 -0
- package/src/clis/jike/feed.ts +1 -1
- package/src/clis/jike/search.ts +1 -1
- package/src/clis/linkedin/search.ts +5 -4
- package/src/clis/linkedin/timeline.test.ts +99 -0
- package/src/clis/linkedin/timeline.ts +532 -0
- package/src/clis/medium/feed.ts +1 -1
- package/src/clis/medium/search.ts +1 -1
- package/src/clis/medium/user.ts +1 -1
- package/src/clis/medium/{shared.ts → utils.ts} +2 -1
- package/src/clis/pixiv/detail.yaml +49 -0
- package/src/clis/pixiv/download.test.ts +114 -0
- package/src/clis/pixiv/download.ts +91 -0
- package/src/clis/pixiv/illusts.test.ts +115 -0
- package/src/clis/pixiv/illusts.ts +78 -0
- package/src/clis/pixiv/ranking.yaml +53 -0
- package/src/clis/pixiv/search.test.ts +97 -0
- package/src/clis/pixiv/search.ts +53 -0
- package/src/clis/pixiv/test-utils.ts +29 -0
- package/src/clis/pixiv/user.yaml +46 -0
- package/src/clis/pixiv/utils.ts +62 -0
- package/src/clis/reddit/comment.ts +2 -1
- package/src/clis/reddit/read.test.ts +34 -0
- package/src/clis/reddit/read.ts +4 -3
- package/src/clis/reddit/save.ts +2 -1
- package/src/clis/reddit/saved.ts +6 -2
- package/src/clis/reddit/subscribe.ts +2 -1
- package/src/clis/reddit/upvote.ts +2 -1
- package/src/clis/reddit/upvoted.ts +6 -2
- package/src/clis/sinablog/article.ts +1 -1
- package/src/clis/sinablog/hot.ts +1 -1
- package/src/clis/sinablog/user.ts +1 -1
- package/src/clis/substack/feed.ts +1 -1
- package/src/clis/substack/publication.ts +1 -1
- package/src/clis/substack/search.ts +3 -2
- package/src/clis/substack/{shared.ts → utils.ts} +3 -2
- package/src/clis/tiktok/search.yaml +2 -1
- package/src/clis/twitter/accept.ts +2 -1
- package/src/clis/twitter/article.ts +3 -1
- package/src/clis/twitter/block.ts +2 -1
- package/src/clis/twitter/bookmark.ts +2 -1
- package/src/clis/twitter/bookmarks.ts +3 -2
- package/src/clis/twitter/delete.ts +2 -1
- package/src/clis/twitter/follow.ts +2 -1
- package/src/clis/twitter/followers.ts +3 -2
- package/src/clis/twitter/following.ts +3 -2
- package/src/clis/twitter/hide-reply.ts +2 -1
- package/src/clis/twitter/like.ts +2 -1
- package/src/clis/twitter/notifications.ts +2 -1
- package/src/clis/twitter/post.ts +2 -1
- package/src/clis/twitter/profile.ts +4 -2
- package/src/clis/twitter/reply-dm.ts +2 -1
- package/src/clis/twitter/reply.ts +2 -1
- package/src/clis/twitter/search.test.ts +113 -0
- package/src/clis/twitter/search.ts +38 -14
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/timeline.ts +3 -2
- package/src/clis/twitter/trending.ts +3 -2
- package/src/clis/twitter/unblock.ts +2 -1
- package/src/clis/twitter/unbookmark.ts +2 -1
- package/src/clis/twitter/unfollow.ts +2 -1
- package/src/clis/v2ex/daily.ts +3 -2
- package/src/clis/v2ex/me.ts +3 -2
- package/src/clis/v2ex/notifications.ts +3 -4
- package/src/clis/web/read.ts +210 -0
- package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
- package/src/clis/xueqiu/danjuan-utils.ts +176 -0
- package/src/clis/xueqiu/fund-holdings.ts +32 -0
- package/src/clis/xueqiu/fund-snapshot.ts +27 -0
- package/src/clis/youtube/transcript.ts +5 -4
- package/src/clis/youtube/video.ts +3 -2
- package/src/constants.ts +3 -0
- package/src/daemon.ts +7 -5
- package/src/discovery.ts +12 -34
- package/src/doctor.ts +5 -3
- package/src/download/index.test.ts +93 -2
- package/src/download/index.ts +44 -23
- package/src/download/media-download.ts +5 -3
- package/src/engine.test.ts +84 -3
- package/src/execution.ts +62 -46
- package/src/explore.ts +21 -90
- package/src/external-clis.yaml +0 -8
- package/src/external.test.ts +9 -0
- package/src/external.ts +12 -10
- package/src/generate.ts +4 -41
- package/src/hooks.test.ts +126 -0
- package/src/hooks.ts +90 -0
- package/src/interceptor.ts +73 -23
- package/src/main.ts +2 -0
- package/src/output.ts +14 -6
- package/src/pipeline/executor.ts +1 -1
- package/src/pipeline/steps/browser.ts +1 -3
- package/src/pipeline/steps/download.test.ts +136 -0
- package/src/pipeline/steps/download.ts +47 -34
- package/src/pipeline/steps/fetch.test.ts +179 -0
- package/src/pipeline/steps/fetch.ts +39 -23
- package/src/pipeline/steps/transform.ts +2 -6
- package/src/pipeline/template.test.ts +28 -0
- package/src/pipeline/template.ts +67 -79
- package/src/pipeline/transform.test.ts +20 -0
- package/src/plugin.test.ts +251 -3
- package/src/plugin.ts +265 -21
- package/src/record.ts +12 -84
- package/src/registry-api.ts +2 -0
- package/src/registry.ts +7 -4
- package/src/runtime.ts +14 -4
- package/src/snapshotFormatter.ts +43 -121
- package/src/utils.ts +39 -0
- package/src/validate.ts +3 -6
- package/src/yaml-schema.ts +28 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
- package/tests/e2e/plugin-management.test.ts +137 -0
- package/tests/e2e/public-commands.test.ts +34 -1
- package/vitest.config.ts +19 -1
- package/.github/workflows/pkg-pr-new.yml +0 -30
- package/dist/clis/douban/shared.d.ts +0 -4
- package/dist/clis/douban/shared.js +0 -155
- package/src/clis/douban/shared.ts +0 -165
- /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/boss/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
- /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/jike/{shared.js → utils.js} +0 -0
- /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
- /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
- /package/src/clis/boss/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
- /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
- /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
package/src/snapshotFormatter.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Aria snapshot formatter: parses Playwright MCP snapshot text into clean format.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1. Parse & filter: strip annotations, metadata, noise
|
|
6
|
-
* 2. Deduplicate: generic/text
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
9
|
-
* 5. Prune: empty containers (iterative bottom-up)
|
|
10
|
-
* 6. Collapse: single-child containers
|
|
4
|
+
* 4-pass pipeline:
|
|
5
|
+
* 1. Parse & filter: strip annotations, metadata, noise, ads, boilerplate subtrees
|
|
6
|
+
* 2. Deduplicate: generic/text parent match, heading+link, nested identical links
|
|
7
|
+
* 3. Prune: empty containers (iterative bottom-up)
|
|
8
|
+
* 4. Collapse: single-child containers
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
interactive?: boolean;
|
|
15
|
-
compact?: boolean;
|
|
16
|
-
maxDepth?: number;
|
|
17
|
-
maxTextLength?: number;
|
|
18
|
-
}
|
|
11
|
+
import type { SnapshotOptions } from './types.js';
|
|
19
12
|
|
|
20
13
|
const DEFAULT_MAX_TEXT_LENGTH = 200;
|
|
21
14
|
|
|
@@ -199,19 +192,18 @@ interface Entry {
|
|
|
199
192
|
trailingText: string;
|
|
200
193
|
isInteractive: boolean;
|
|
201
194
|
isLandmark: boolean;
|
|
202
|
-
isSubtreeSkip: boolean; // ad nodes or boilerplate — skip entire subtree
|
|
203
195
|
}
|
|
204
196
|
|
|
205
|
-
export function formatSnapshot(raw: string, opts:
|
|
197
|
+
export function formatSnapshot(raw: string, opts: SnapshotOptions = {}): string {
|
|
206
198
|
if (!raw || typeof raw !== 'string') return '';
|
|
207
199
|
|
|
208
200
|
const maxTextLen = opts.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH;
|
|
209
201
|
const lines = raw.split('\n');
|
|
210
202
|
|
|
211
|
-
// === Pass 1: Parse, filter, and collect entries ===
|
|
212
|
-
const
|
|
203
|
+
// === Pass 1: Parse, filter, and collect entries (merged with ad/boilerplate subtree skip) ===
|
|
204
|
+
const parsed: Entry[] = [];
|
|
213
205
|
let refCounter = 0;
|
|
214
|
-
let skipUntilDepth = -1;
|
|
206
|
+
let skipUntilDepth = -1;
|
|
215
207
|
|
|
216
208
|
for (let i = 0; i < lines.length; i++) {
|
|
217
209
|
const line = lines[i];
|
|
@@ -220,148 +212,88 @@ export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
|
|
|
220
212
|
const indent = line.length - line.trimStart().length;
|
|
221
213
|
const depth = Math.floor(indent / 2);
|
|
222
214
|
|
|
223
|
-
//
|
|
215
|
+
// Subtree skip zone (noise roles, ads, boilerplate)
|
|
224
216
|
if (skipUntilDepth >= 0) {
|
|
225
|
-
if (depth > skipUntilDepth) continue;
|
|
226
|
-
skipUntilDepth = -1;
|
|
217
|
+
if (depth > skipUntilDepth) continue;
|
|
218
|
+
skipUntilDepth = -1;
|
|
227
219
|
}
|
|
228
220
|
|
|
229
221
|
let content = line.trimStart();
|
|
230
|
-
|
|
231
|
-
// Strip leading "- "
|
|
232
|
-
if (content.startsWith('- ')) {
|
|
233
|
-
content = content.slice(2);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Skip metadata lines
|
|
222
|
+
if (content.startsWith('- ')) content = content.slice(2);
|
|
237
223
|
if (isMetadataLine(content)) continue;
|
|
238
|
-
|
|
239
|
-
// Apply maxDepth filter
|
|
240
224
|
if (opts.maxDepth !== undefined && depth > opts.maxDepth) continue;
|
|
241
225
|
|
|
242
226
|
const { role, text, hasText, trailingText } = parseLine(content);
|
|
243
227
|
|
|
244
|
-
// Skip noise nodes
|
|
245
228
|
if (isNoiseNode(role, hasText, text, trailingText)) continue;
|
|
246
229
|
|
|
247
|
-
//
|
|
248
|
-
if (SUBTREE_NOISE_ROLES.has(role)) {
|
|
249
|
-
skipUntilDepth = depth;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
230
|
+
// Subtree noise roles (contentinfo footer, etc.)
|
|
231
|
+
if (SUBTREE_NOISE_ROLES.has(role)) { skipUntilDepth = depth; continue; }
|
|
252
232
|
|
|
253
|
-
//
|
|
254
|
-
|
|
233
|
+
// Ads and boilerplate — skip entire subtree (merged from old Pass 2)
|
|
234
|
+
if (isAdNode(text, trailingText) || isBoilerplateNode(text)) { skipUntilDepth = depth; continue; }
|
|
255
235
|
|
|
256
|
-
|
|
257
|
-
const isSubtreeSkip = isAdNode(text, trailingText) || isBoilerplateNode(text);
|
|
236
|
+
content = stripAnnotations(content);
|
|
258
237
|
|
|
259
|
-
// Interactive mode filter
|
|
260
238
|
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
261
239
|
const isLandmark = LANDMARK_ROLES.has(role);
|
|
262
|
-
|
|
263
240
|
if (opts.interactive && !isInteractive && !isLandmark && !hasText) continue;
|
|
264
241
|
|
|
265
|
-
// Compact mode
|
|
266
242
|
if (opts.compact) {
|
|
267
|
-
content = content
|
|
268
|
-
.replace(/\s*\[.*?\]\s*/g, ' ')
|
|
269
|
-
.replace(/\s+/g, ' ')
|
|
270
|
-
.trim();
|
|
243
|
+
content = content.replace(/\s*\[.*?\]\s*/g, ' ').replace(/\s+/g, ' ').trim();
|
|
271
244
|
}
|
|
272
|
-
|
|
273
|
-
// Text truncation
|
|
274
245
|
if (maxTextLen > 0 && content.length > maxTextLen) {
|
|
275
246
|
content = content.slice(0, maxTextLen) + '…';
|
|
276
247
|
}
|
|
277
|
-
|
|
278
|
-
// Assign refs to interactive elements
|
|
279
248
|
if (isInteractive) {
|
|
280
249
|
refCounter++;
|
|
281
250
|
content = `[@${refCounter}] ${content}`;
|
|
282
251
|
}
|
|
283
252
|
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// === Pass 2: Remove subtree-skip nodes (ads, boilerplate, contentinfo) ===
|
|
288
|
-
let noAds: Entry[] = [];
|
|
289
|
-
for (let i = 0; i < entries.length; i++) {
|
|
290
|
-
const entry = entries[i];
|
|
291
|
-
if (entry.isSubtreeSkip) {
|
|
292
|
-
const skipDepth = entry.depth;
|
|
293
|
-
i++;
|
|
294
|
-
while (i < entries.length && entries[i].depth > skipDepth) {
|
|
295
|
-
i++;
|
|
296
|
-
}
|
|
297
|
-
i--;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
noAds.push(entry);
|
|
253
|
+
parsed.push({ depth, content, role, text, trailingText, isInteractive, isLandmark });
|
|
301
254
|
}
|
|
302
255
|
|
|
303
|
-
// === Pass
|
|
304
|
-
|
|
305
|
-
for (let i = 0; i <
|
|
306
|
-
const entry =
|
|
256
|
+
// === Pass 2: Deduplicate (merged: generic/text parent match + heading+link + nested links) ===
|
|
257
|
+
const deduped: Entry[] = [];
|
|
258
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
259
|
+
const entry = parsed[i];
|
|
307
260
|
|
|
261
|
+
// Dedup: generic/text child matching parent label
|
|
308
262
|
if (entry.role === 'generic' || entry.role === 'text') {
|
|
309
263
|
let parent: Entry | undefined;
|
|
310
264
|
for (let j = deduped.length - 1; j >= 0; j--) {
|
|
311
|
-
if (deduped[j].depth < entry.depth) {
|
|
312
|
-
parent = deduped[j];
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
265
|
+
if (deduped[j].depth < entry.depth) { parent = deduped[j]; break; }
|
|
315
266
|
if (deduped[j].depth === entry.depth) break;
|
|
316
267
|
}
|
|
317
|
-
|
|
318
268
|
if (parent) {
|
|
319
269
|
const childText = entry.trailingText || entry.text;
|
|
320
|
-
if (childText && parent.text && childText === parent.text)
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
270
|
+
if (childText && parent.text && childText === parent.text) continue;
|
|
323
271
|
}
|
|
324
272
|
}
|
|
325
273
|
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// === Pass 4: Deduplicate heading + child link with identical label ===
|
|
330
|
-
// Pattern: heading "Title": → link "Title": (same text) → skip the link
|
|
331
|
-
const deduped2: Entry[] = [];
|
|
332
|
-
for (let i = 0; i < deduped.length; i++) {
|
|
333
|
-
const entry = deduped[i];
|
|
334
|
-
|
|
274
|
+
// Dedup: heading + child link with identical label
|
|
335
275
|
if (entry.role === 'heading' && entry.text) {
|
|
336
|
-
const next =
|
|
276
|
+
const next = parsed[i + 1];
|
|
337
277
|
if (next && next.role === 'link' && next.text === entry.text && next.depth === entry.depth + 1) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
i++; // skip the link
|
|
278
|
+
deduped.push(entry);
|
|
279
|
+
i++; // skip the link, preserve its children
|
|
341
280
|
continue;
|
|
342
281
|
}
|
|
343
282
|
}
|
|
344
283
|
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// === Pass 5: Deduplicate nested identical links ===
|
|
349
|
-
const deduped3: Entry[] = [];
|
|
350
|
-
for (let i = 0; i < deduped2.length; i++) {
|
|
351
|
-
const entry = deduped2[i];
|
|
352
|
-
|
|
284
|
+
// Dedup: nested identical links (skip parent, keep child)
|
|
353
285
|
if (entry.role === 'link' && entry.text) {
|
|
354
|
-
const next =
|
|
286
|
+
const next = parsed[i + 1];
|
|
355
287
|
if (next && next.role === 'link' && next.text === entry.text && next.depth === entry.depth + 1) {
|
|
356
|
-
continue;
|
|
288
|
+
continue;
|
|
357
289
|
}
|
|
358
290
|
}
|
|
359
291
|
|
|
360
|
-
|
|
292
|
+
deduped.push(entry);
|
|
361
293
|
}
|
|
362
294
|
|
|
363
|
-
// === Pass
|
|
364
|
-
let current =
|
|
295
|
+
// === Pass 3: Iteratively prune empty containers (bottom-up) ===
|
|
296
|
+
let current = deduped;
|
|
365
297
|
let changed = true;
|
|
366
298
|
while (changed) {
|
|
367
299
|
changed = false;
|
|
@@ -372,22 +304,16 @@ export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
|
|
|
372
304
|
let hasChildren = false;
|
|
373
305
|
for (let j = i + 1; j < current.length; j++) {
|
|
374
306
|
if (current[j].depth <= entry.depth) break;
|
|
375
|
-
if (current[j].depth > entry.depth) {
|
|
376
|
-
hasChildren = true;
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
if (!hasChildren) {
|
|
381
|
-
changed = true;
|
|
382
|
-
continue;
|
|
307
|
+
if (current[j].depth > entry.depth) { hasChildren = true; break; }
|
|
383
308
|
}
|
|
309
|
+
if (!hasChildren) { changed = true; continue; }
|
|
384
310
|
}
|
|
385
311
|
next.push(entry);
|
|
386
312
|
}
|
|
387
313
|
current = next;
|
|
388
314
|
}
|
|
389
315
|
|
|
390
|
-
// === Pass
|
|
316
|
+
// === Pass 4: Collapse single-child containers ===
|
|
391
317
|
const collapsed: Entry[] = [];
|
|
392
318
|
for (let i = 0; i < current.length; i++) {
|
|
393
319
|
const entry = current[i];
|
|
@@ -408,17 +334,13 @@ export function formatSnapshot(raw: string, opts: FormatOptions = {}): string {
|
|
|
408
334
|
let hasGrandchildren = false;
|
|
409
335
|
for (let j = childIdx + 1; j < current.length; j++) {
|
|
410
336
|
if (current[j].depth <= child.depth) break;
|
|
411
|
-
if (current[j].depth > child.depth) {
|
|
412
|
-
hasGrandchildren = true;
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
337
|
+
if (current[j].depth > child.depth) { hasGrandchildren = true; break; }
|
|
415
338
|
}
|
|
416
339
|
|
|
417
340
|
if (!hasGrandchildren) {
|
|
418
|
-
const mergedContent = entry.content.replace(/:$/, '') + ' > ' + child.content;
|
|
419
341
|
collapsed.push({
|
|
420
342
|
...entry,
|
|
421
|
-
content:
|
|
343
|
+
content: entry.content.replace(/:$/, '') + ' > ' + child.content,
|
|
422
344
|
role: child.role,
|
|
423
345
|
text: child.text,
|
|
424
346
|
trailingText: child.trailingText,
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions used across the codebase.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
|
|
8
|
+
/** Type guard: checks if a value is a non-null, non-array object. */
|
|
9
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Simple async concurrency limiter. */
|
|
14
|
+
export async function mapConcurrent<T, R>(
|
|
15
|
+
items: T[],
|
|
16
|
+
limit: number,
|
|
17
|
+
fn: (item: T, index: number) => Promise<R>,
|
|
18
|
+
): Promise<R[]> {
|
|
19
|
+
const results: R[] = new Array(items.length);
|
|
20
|
+
let index = 0;
|
|
21
|
+
|
|
22
|
+
async function worker() {
|
|
23
|
+
while (index < items.length) {
|
|
24
|
+
const i = index++;
|
|
25
|
+
results[i] = await fn(items[i], i);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
30
|
+
await Promise.all(workers);
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Save a base64-encoded string to a file, creating parent directories as needed. */
|
|
35
|
+
export async function saveBase64ToFile(base64: string, filePath: string): Promise<void> {
|
|
36
|
+
const dir = path.dirname(filePath);
|
|
37
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
38
|
+
await fs.promises.writeFile(filePath, Buffer.from(base64, 'base64'));
|
|
39
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { getErrorMessage } from './errors.js';
|
|
|
6
6
|
|
|
7
7
|
/** All recognized pipeline step names */
|
|
8
8
|
const KNOWN_STEP_NAMES = new Set([
|
|
9
|
-
'navigate', 'click', 'type', 'wait', 'press', 'snapshot',
|
|
9
|
+
'navigate', 'click', 'type', 'wait', 'press', 'snapshot',
|
|
10
10
|
'fetch', 'evaluate',
|
|
11
11
|
'select', 'map', 'filter', 'sort', 'limit',
|
|
12
|
-
'intercept', 'tap',
|
|
12
|
+
'intercept', 'tap', 'download',
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
15
|
export interface FileValidationResult {
|
|
@@ -34,10 +34,7 @@ interface ValidatedYamlCliDefinition {
|
|
|
34
34
|
args?: Record<string, unknown>;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
|
-
}
|
|
40
|
-
|
|
37
|
+
import { isRecord } from './utils.js';
|
|
41
38
|
|
|
42
39
|
|
|
43
40
|
export function validateClisWithTarget(dirs: string[], target?: string): ValidationReport {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared YAML CLI definition types.
|
|
3
|
+
* Used by both discovery.ts (runtime) and build-manifest.ts (build-time).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface YamlArgDefinition {
|
|
7
|
+
type?: string;
|
|
8
|
+
default?: unknown;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
positional?: boolean;
|
|
11
|
+
description?: string;
|
|
12
|
+
help?: string;
|
|
13
|
+
choices?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface YamlCliDefinition {
|
|
17
|
+
site?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
domain?: string;
|
|
21
|
+
strategy?: string;
|
|
22
|
+
browser?: boolean;
|
|
23
|
+
args?: Record<string, YamlArgDefinition>;
|
|
24
|
+
columns?: string[];
|
|
25
|
+
pipeline?: Record<string, unknown>[];
|
|
26
|
+
timeout?: number;
|
|
27
|
+
navigateBefore?: boolean | string;
|
|
28
|
+
}
|
|
@@ -113,6 +113,31 @@ describe('login-required commands — graceful failure', () => {
|
|
|
113
113
|
await expectGracefulAuthFailure(['xiaohongshu', 'notifications', '--limit', '3', '-f', 'json'], 'xiaohongshu notifications');
|
|
114
114
|
}, 60_000);
|
|
115
115
|
|
|
116
|
+
// ── pixiv (requires login) ──
|
|
117
|
+
it('pixiv ranking fails gracefully without login', async () => {
|
|
118
|
+
await expectGracefulAuthFailure(['pixiv', 'ranking', '--limit', '3', '-f', 'json'], 'pixiv ranking');
|
|
119
|
+
}, 60_000);
|
|
120
|
+
|
|
121
|
+
it('pixiv search fails gracefully without login', async () => {
|
|
122
|
+
await expectGracefulAuthFailure(['pixiv', 'search', '初音ミク', '--limit', '3', '-f', 'json'], 'pixiv search');
|
|
123
|
+
}, 60_000);
|
|
124
|
+
|
|
125
|
+
it('pixiv user fails gracefully without login', async () => {
|
|
126
|
+
await expectGracefulAuthFailure(['pixiv', 'user', '11', '-f', 'json'], 'pixiv user');
|
|
127
|
+
}, 60_000);
|
|
128
|
+
|
|
129
|
+
it('pixiv illusts fails gracefully without login', async () => {
|
|
130
|
+
await expectGracefulAuthFailure(['pixiv', 'illusts', '11', '--limit', '3', '-f', 'json'], 'pixiv illusts');
|
|
131
|
+
}, 60_000);
|
|
132
|
+
|
|
133
|
+
it('pixiv detail fails gracefully without login', async () => {
|
|
134
|
+
await expectGracefulAuthFailure(['pixiv', 'detail', '123456', '-f', 'json'], 'pixiv detail');
|
|
135
|
+
}, 60_000);
|
|
136
|
+
|
|
137
|
+
it('pixiv download fails gracefully without login', async () => {
|
|
138
|
+
await expectGracefulAuthFailure(['pixiv', 'download', '123456', '--output', '/tmp/pixiv-e2e-test', '-f', 'json'], 'pixiv download');
|
|
139
|
+
}, 60_000);
|
|
140
|
+
|
|
116
141
|
// ── yollomi (requires login session) ──
|
|
117
142
|
it('yollomi generate fails gracefully without login', async () => {
|
|
118
143
|
await expectGracefulAuthFailure(['yollomi', 'generate', 'a cute cat', '--no-download', '-f', 'json'], 'yollomi generate');
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E integration tests for plugin management commands.
|
|
3
|
+
* Uses a real GitHub plugin (opencli-plugin-hot-digest) to verify the full
|
|
4
|
+
* install → list → update → uninstall lifecycle in an isolated HOME.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, afterAll } from 'vitest';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import { runCli, parseJsonOutput } from './helpers.js';
|
|
12
|
+
|
|
13
|
+
const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-plugin-e2e-'));
|
|
14
|
+
const OPENCLI_HOME = path.join(TEST_HOME, '.opencli');
|
|
15
|
+
const PLUGINS_DIR = path.join(OPENCLI_HOME, 'plugins');
|
|
16
|
+
const PLUGIN_SOURCE = 'github:ByteYue/opencli-plugin-hot-digest';
|
|
17
|
+
const PLUGIN_NAME = 'hot-digest';
|
|
18
|
+
const PLUGIN_DIR = path.join(PLUGINS_DIR, PLUGIN_NAME);
|
|
19
|
+
const LOCK_FILE = path.join(OPENCLI_HOME, 'plugins.lock.json');
|
|
20
|
+
|
|
21
|
+
function runPluginCli(
|
|
22
|
+
args: string[],
|
|
23
|
+
opts: { timeout?: number; env?: Record<string, string> } = {},
|
|
24
|
+
) {
|
|
25
|
+
return runCli(args, {
|
|
26
|
+
...opts,
|
|
27
|
+
env: {
|
|
28
|
+
HOME: TEST_HOME,
|
|
29
|
+
USERPROFILE: TEST_HOME,
|
|
30
|
+
...opts.env,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('plugin management E2E', () => {
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
fs.rmSync(TEST_HOME, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ── plugin list (empty) ──
|
|
41
|
+
it('plugin list shows "No plugins installed" when none exist', async () => {
|
|
42
|
+
const { stdout, code } = await runPluginCli(['plugin', 'list']);
|
|
43
|
+
expect(code).toBe(0);
|
|
44
|
+
expect(stdout).toContain('No plugins installed');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── plugin install ──
|
|
48
|
+
it('plugin install clones and sets up a real plugin', async () => {
|
|
49
|
+
const { stdout, code } = await runPluginCli(['plugin', 'install', PLUGIN_SOURCE], {
|
|
50
|
+
timeout: 60_000,
|
|
51
|
+
});
|
|
52
|
+
expect(code).toBe(0);
|
|
53
|
+
expect(stdout).toContain('installed successfully');
|
|
54
|
+
expect(stdout).toContain(PLUGIN_NAME);
|
|
55
|
+
|
|
56
|
+
// Verify the plugin directory was created
|
|
57
|
+
expect(fs.existsSync(PLUGIN_DIR)).toBe(true);
|
|
58
|
+
|
|
59
|
+
// Verify lock file was updated
|
|
60
|
+
const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
|
|
61
|
+
expect(lock[PLUGIN_NAME]).toBeDefined();
|
|
62
|
+
expect(lock[PLUGIN_NAME].commitHash).toBeTruthy();
|
|
63
|
+
expect(lock[PLUGIN_NAME].source).toContain('opencli-plugin-hot-digest');
|
|
64
|
+
expect(lock[PLUGIN_NAME].installedAt).toBeTruthy();
|
|
65
|
+
}, 60_000);
|
|
66
|
+
|
|
67
|
+
// ── plugin list (after install) ──
|
|
68
|
+
it('plugin list shows the installed plugin', async () => {
|
|
69
|
+
const { stdout, code } = await runPluginCli(['plugin', 'list']);
|
|
70
|
+
expect(code).toBe(0);
|
|
71
|
+
expect(stdout).toContain(PLUGIN_NAME);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('plugin list -f json returns structured data', async () => {
|
|
75
|
+
const { stdout, code } = await runPluginCli(['plugin', 'list', '-f', 'json']);
|
|
76
|
+
expect(code).toBe(0);
|
|
77
|
+
const data = parseJsonOutput(stdout);
|
|
78
|
+
expect(Array.isArray(data)).toBe(true);
|
|
79
|
+
|
|
80
|
+
const plugin = data.find((p: any) => p.name === PLUGIN_NAME);
|
|
81
|
+
expect(plugin).toBeDefined();
|
|
82
|
+
expect(plugin.name).toBe(PLUGIN_NAME);
|
|
83
|
+
expect(Array.isArray(plugin.commands)).toBe(true);
|
|
84
|
+
expect(plugin.commands.length).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── plugin update ──
|
|
88
|
+
it('plugin update succeeds on an installed plugin', async () => {
|
|
89
|
+
const { stdout, code } = await runPluginCli(['plugin', 'update', PLUGIN_NAME], {
|
|
90
|
+
timeout: 30_000,
|
|
91
|
+
});
|
|
92
|
+
expect(code).toBe(0);
|
|
93
|
+
expect(stdout).toContain('updated successfully');
|
|
94
|
+
|
|
95
|
+
// Verify lock file has updatedAt
|
|
96
|
+
const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
|
|
97
|
+
expect(lock[PLUGIN_NAME].updatedAt).toBeTruthy();
|
|
98
|
+
}, 30_000);
|
|
99
|
+
|
|
100
|
+
// ── plugin uninstall ──
|
|
101
|
+
it('plugin uninstall removes the plugin', async () => {
|
|
102
|
+
const { stdout, code } = await runPluginCli(['plugin', 'uninstall', PLUGIN_NAME]);
|
|
103
|
+
expect(code).toBe(0);
|
|
104
|
+
expect(stdout).toContain('uninstalled');
|
|
105
|
+
|
|
106
|
+
// Verify directory was removed
|
|
107
|
+
expect(fs.existsSync(PLUGIN_DIR)).toBe(false);
|
|
108
|
+
|
|
109
|
+
// Verify lock entry was removed
|
|
110
|
+
const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
|
|
111
|
+
expect(lock[PLUGIN_NAME]).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── error paths ──
|
|
115
|
+
it('plugin install rejects invalid source', async () => {
|
|
116
|
+
const { stderr, code } = await runPluginCli(['plugin', 'install', 'invalid-source-format']);
|
|
117
|
+
expect(code).toBe(1);
|
|
118
|
+
expect(stderr).toContain('Invalid plugin source');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('plugin uninstall rejects non-existent plugin', async () => {
|
|
122
|
+
const { stderr, code } = await runPluginCli(['plugin', 'uninstall', '__nonexistent_plugin_xyz__']);
|
|
123
|
+
expect(code).toBe(1);
|
|
124
|
+
expect(stderr).toContain('not installed');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('plugin update rejects non-existent plugin', async () => {
|
|
128
|
+
const { stderr, code } = await runPluginCli(['plugin', 'update', '__nonexistent_plugin_xyz__']);
|
|
129
|
+
expect(code).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('plugin update without name or --all shows error', async () => {
|
|
133
|
+
const { stderr, code } = await runPluginCli(['plugin', 'update']);
|
|
134
|
+
expect(code).toBe(1);
|
|
135
|
+
expect(stderr).toContain('specify a plugin name');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -16,7 +16,8 @@ function isExpectedChineseSiteRestriction(code: number, stderr: string): boolean
|
|
|
16
16
|
|
|
17
17
|
function isExpectedApplePodcastsRestriction(code: number, stderr: string): boolean {
|
|
18
18
|
if (code === 0) return false;
|
|
19
|
-
return /Error \[FETCH_ERROR\]: (Charts API HTTP \d+|Unable to reach Apple Podcasts charts)/.test(stderr)
|
|
19
|
+
return /Error \[FETCH_ERROR\]: (Charts API HTTP \d+|Unable to reach Apple Podcasts charts)/.test(stderr)
|
|
20
|
+
|| stderr === ''; // timeout killed the process before any output
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function isExpectedGoogleRestriction(code: number, stderr: string): boolean {
|
|
@@ -488,4 +489,36 @@ describe('public commands E2E', () => {
|
|
|
488
489
|
expect(data.length).toBeGreaterThan(0);
|
|
489
490
|
expect(data.every((d: any) => d.type === 'image')).toBe(true);
|
|
490
491
|
}, 30_000);
|
|
492
|
+
|
|
493
|
+
// ── dictionary (public API, browser: false) ──
|
|
494
|
+
it('dictionary search returns word definitions', async () => {
|
|
495
|
+
const { stdout, code } = await runCli(['dictionary', 'search', 'serendipity', '-f', 'json']);
|
|
496
|
+
expect(code).toBe(0);
|
|
497
|
+
const data = parseJsonOutput(stdout);
|
|
498
|
+
expect(Array.isArray(data)).toBe(true);
|
|
499
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
500
|
+
expect(data[0]).toHaveProperty('word', 'serendipity');
|
|
501
|
+
expect(data[0]).toHaveProperty('phonetic');
|
|
502
|
+
expect(data[0]).toHaveProperty('definition');
|
|
503
|
+
}, 30_000);
|
|
504
|
+
|
|
505
|
+
it('dictionary synonyms returns synonyms', async () => {
|
|
506
|
+
const { stdout, code } = await runCli(['dictionary', 'synonyms', 'serendipity', '-f', 'json']);
|
|
507
|
+
expect(code).toBe(0);
|
|
508
|
+
const data = parseJsonOutput(stdout);
|
|
509
|
+
expect(Array.isArray(data)).toBe(true);
|
|
510
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
511
|
+
expect(data[0]).toHaveProperty('word', 'serendipity');
|
|
512
|
+
expect(data[0]).toHaveProperty('synonyms');
|
|
513
|
+
}, 30_000);
|
|
514
|
+
|
|
515
|
+
it('dictionary examples returns examples', async () => {
|
|
516
|
+
const { stdout, code } = await runCli(['dictionary', 'examples', 'perfect', '-f', 'json']);
|
|
517
|
+
expect(code).toBe(0);
|
|
518
|
+
const data = parseJsonOutput(stdout);
|
|
519
|
+
expect(Array.isArray(data)).toBe(true);
|
|
520
|
+
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
521
|
+
expect(data[0]).toHaveProperty('word', 'perfect');
|
|
522
|
+
expect(data[0]).toHaveProperty('example');
|
|
523
|
+
}, 30_000);
|
|
491
524
|
});
|
package/vitest.config.ts
CHANGED
|
@@ -7,19 +7,37 @@ export default defineConfig({
|
|
|
7
7
|
test: {
|
|
8
8
|
name: 'unit',
|
|
9
9
|
include: ['src/**/*.test.ts'],
|
|
10
|
+
exclude: ['src/clis/**/*.test.ts'],
|
|
10
11
|
// Run unit tests before e2e tests to avoid project-level contention in CI.
|
|
11
12
|
sequence: {
|
|
12
13
|
groupOrder: 0,
|
|
13
14
|
},
|
|
14
15
|
},
|
|
15
16
|
},
|
|
17
|
+
{
|
|
18
|
+
test: {
|
|
19
|
+
name: 'adapter',
|
|
20
|
+
include: [
|
|
21
|
+
'src/clis/zhihu/**/*.test.ts',
|
|
22
|
+
'src/clis/twitter/**/*.test.ts',
|
|
23
|
+
'src/clis/reddit/**/*.test.ts',
|
|
24
|
+
'src/clis/bilibili/**/*.test.ts',
|
|
25
|
+
'src/clis/linkedin/**/*.test.ts',
|
|
26
|
+
'src/clis/grok/**/*.test.ts',
|
|
27
|
+
'src/clis/pixiv/**/*.test.ts',
|
|
28
|
+
],
|
|
29
|
+
sequence: {
|
|
30
|
+
groupOrder: 1,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
16
34
|
{
|
|
17
35
|
test: {
|
|
18
36
|
name: 'e2e',
|
|
19
37
|
include: ['tests/**/*.test.ts'],
|
|
20
38
|
maxWorkers: 2,
|
|
21
39
|
sequence: {
|
|
22
|
-
groupOrder:
|
|
40
|
+
groupOrder: 2,
|
|
23
41
|
},
|
|
24
42
|
},
|
|
25
43
|
},
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: Publish Any Commit
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main, dev]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main, dev]
|
|
8
|
-
|
|
9
|
-
permissions: {}
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
if: ${{ vars.PKG_PR_NEW_ENABLED == 'true' }}
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v6
|
|
17
|
-
|
|
18
|
-
- uses: actions/setup-node@v6
|
|
19
|
-
with:
|
|
20
|
-
node-version: '22'
|
|
21
|
-
cache: 'npm'
|
|
22
|
-
|
|
23
|
-
- name: Install dependencies
|
|
24
|
-
run: npm ci
|
|
25
|
-
|
|
26
|
-
- name: Build
|
|
27
|
-
run: npm run build
|
|
28
|
-
|
|
29
|
-
- name: Publish to pkg.pr.new
|
|
30
|
-
run: npx pkg-pr-new publish
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { IPage } from '../../types.js';
|
|
2
|
-
export declare function loadDoubanBookHot(page: IPage, limit: number): Promise<any[]>;
|
|
3
|
-
export declare function loadDoubanMovieHot(page: IPage, limit: number): Promise<any[]>;
|
|
4
|
-
export declare function searchDouban(page: IPage, type: string, keyword: string, limit: number): Promise<any[]>;
|