@jackwener/opencli 1.7.12 → 1.7.14
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/README.md +8 -7
- package/README.zh-CN.md +9 -8
- package/cli-manifest.json +12128 -6665
- package/clis/1point3acres/digest.js +35 -0
- package/clis/1point3acres/forum.js +51 -0
- package/clis/1point3acres/forums.js +44 -0
- package/clis/1point3acres/hot.js +35 -0
- package/clis/1point3acres/latest.js +35 -0
- package/clis/1point3acres/notifications.js +64 -0
- package/clis/1point3acres/search.js +71 -0
- package/clis/1point3acres/thread.js +117 -0
- package/clis/1point3acres/user.js +77 -0
- package/clis/1point3acres/utils.js +247 -0
- package/clis/_shared/desktop-commands.js +4 -0
- package/clis/aibase/news.js +110 -0
- package/clis/aibase/news.test.js +59 -0
- package/clis/amazon/discussion.test.js +1 -28
- package/clis/antigravity/watch.js +3 -2
- package/clis/arxiv/author.js +44 -0
- package/clis/baidu-scholar/search.js +0 -1
- package/clis/bbc/topic.js +57 -0
- package/clis/bbc/utils.js +79 -0
- package/clis/chaoxing/assignments.js +1 -1
- package/clis/chaoxing/exams.js +1 -1
- package/clis/chatgpt/ask.js +57 -0
- package/clis/chatgpt/commands.test.js +45 -0
- package/clis/chatgpt/detail.js +46 -0
- package/clis/chatgpt/history.js +39 -0
- package/clis/chatgpt/image.js +12 -11
- package/clis/chatgpt/image.test.js +23 -0
- package/clis/chatgpt/new.js +25 -0
- package/clis/chatgpt/read.js +43 -0
- package/clis/chatgpt/send.js +46 -0
- package/clis/chatgpt/status.js +29 -0
- package/clis/chatgpt/utils.js +294 -4
- package/clis/chatgpt/utils.test.js +13 -0
- package/clis/chatgpt-app/ask.js +6 -3
- package/clis/chatwise/ask.js +16 -43
- package/clis/chatwise/composer.test.js +186 -0
- package/clis/chatwise/send.js +2 -24
- package/clis/chatwise/utils.js +143 -0
- package/clis/claude/ask.js +1 -1
- package/clis/claude/detail.js +1 -0
- package/clis/claude/history.js +1 -0
- package/clis/claude/new.js +1 -0
- package/clis/claude/read.js +1 -0
- package/clis/claude/send.js +1 -0
- package/clis/claude/status.js +1 -0
- package/clis/codex/ask.js +15 -9
- package/clis/codex/history.js +16 -33
- package/clis/codex/projects.js +28 -0
- package/clis/codex/read.js +10 -4
- package/clis/codex/send.js +10 -3
- package/clis/codex/sidebar.js +356 -0
- package/clis/codex/sidebar.test.js +329 -0
- package/clis/coingecko/categories.js +75 -0
- package/clis/coingecko/coin.js +107 -0
- package/clis/coingecko/coingecko.test.js +109 -0
- package/clis/coingecko/derivatives.js +84 -0
- package/clis/coingecko/exchanges.js +74 -0
- package/clis/coingecko/global.js +71 -0
- package/clis/coingecko/top.js +64 -0
- package/clis/coingecko/trending.js +55 -0
- package/clis/coupang/add-to-cart.js +21 -13
- package/clis/coupang/coupang.test.js +159 -0
- package/clis/coupang/product.js +257 -0
- package/clis/coupang/search.js +38 -16
- package/clis/coupang/utils.js +55 -1
- package/clis/crates/crate.js +62 -0
- package/clis/crates/search.js +44 -0
- package/clis/crates/utils.js +72 -0
- package/clis/ctrip/ctrip.test.js +234 -0
- package/clis/ctrip/hotel-suggest.js +45 -0
- package/clis/ctrip/search.js +22 -68
- package/clis/ctrip/utils.js +175 -0
- package/clis/cursor/ask.js +6 -3
- package/clis/dblp/author.js +133 -0
- package/clis/dblp/venue.js +64 -0
- package/clis/deepseek/ask.js +12 -7
- package/clis/deepseek/ask.test.js +13 -13
- package/clis/deepseek/detail.js +38 -0
- package/clis/deepseek/detail.test.js +81 -0
- package/clis/deepseek/history.js +1 -0
- package/clis/deepseek/new.js +1 -0
- package/clis/deepseek/read.js +1 -0
- package/clis/deepseek/send.js +140 -0
- package/clis/deepseek/send.test.js +107 -0
- package/clis/deepseek/status.js +1 -0
- package/clis/deepseek/utils.js +66 -0
- package/clis/deepseek/utils.test.js +107 -1
- package/clis/defillama/defillama.test.js +99 -0
- package/clis/defillama/protocol.js +84 -0
- package/clis/defillama/protocols.js +55 -0
- package/clis/defillama/utils.js +99 -0
- package/clis/devto/latest.js +74 -0
- package/clis/dockerhub/image.js +52 -0
- package/clis/dockerhub/search.js +47 -0
- package/clis/dockerhub/utils.js +100 -0
- package/clis/doubao/ask.js +7 -3
- package/clis/doubao/detail.js +1 -0
- package/clis/doubao/history.js +1 -0
- package/clis/doubao/meeting-summary.js +1 -0
- package/clis/doubao/meeting-transcript.js +1 -0
- package/clis/doubao/new.js +1 -0
- package/clis/doubao/read.js +1 -0
- package/clis/doubao/send.js +1 -0
- package/clis/doubao/status.js +1 -0
- package/clis/douyin/draft.test.js +1 -30
- package/clis/endoflife/endoflife.test.js +51 -0
- package/clis/endoflife/product.js +55 -0
- package/clis/endoflife/utils.js +89 -0
- package/clis/facebook/__fixtures__/notifications-page.html +13 -0
- package/clis/facebook/notifications.js +326 -30
- package/clis/facebook/notifications.test.js +458 -0
- package/clis/flathub/app.js +71 -0
- package/clis/flathub/flathub.test.js +90 -0
- package/clis/flathub/search.js +80 -0
- package/clis/flathub/utils.js +114 -0
- package/clis/gemini/ask.js +7 -3
- package/clis/gemini/ask.test.js +2 -2
- package/clis/gemini/deep-research-result.js +6 -2
- package/clis/gemini/deep-research-result.test.js +15 -14
- package/clis/gemini/deep-research.js +8 -4
- package/clis/gemini/deep-research.test.js +15 -18
- package/clis/gemini/image.js +7 -2
- package/clis/gemini/new.js +1 -0
- package/clis/gemini/utils.js +0 -4
- package/clis/google-scholar/cite.js +0 -1
- package/clis/google-scholar/profile.js +0 -1
- package/clis/google-scholar/search.js +0 -1
- package/clis/goproxy/goproxy.test.js +103 -0
- package/clis/goproxy/module.js +47 -0
- package/clis/goproxy/utils.js +165 -0
- package/clis/goproxy/versions.js +59 -0
- package/clis/gov-law/recent.js +0 -1
- package/clis/gov-law/search.js +0 -1
- package/clis/gov-policy/__fixtures__/recent.html +16 -0
- package/clis/gov-policy/__fixtures__/search.html +41 -0
- package/clis/gov-policy/gov-policy.test.js +224 -0
- package/clis/gov-policy/recent.js +66 -24
- package/clis/gov-policy/search.js +65 -23
- package/clis/gov-policy/utils.js +54 -0
- package/clis/grok/ask.js +49 -265
- package/clis/grok/ask.test.js +21 -46
- package/clis/grok/detail.js +60 -0
- package/clis/grok/history.js +48 -0
- package/clis/grok/{image.ts → image.js} +56 -70
- package/clis/grok/image.test.ts +20 -0
- package/clis/grok/new.js +20 -0
- package/clis/grok/read.js +39 -0
- package/clis/grok/send.js +50 -0
- package/clis/grok/status.js +41 -0
- package/clis/grok/utils.js +326 -0
- package/clis/grok/utils.test.js +103 -0
- package/clis/hf/datasets.js +88 -0
- package/clis/hf/hf.test.js +16 -0
- package/clis/hf/models.js +91 -0
- package/clis/hf/paper.js +79 -0
- package/clis/hf/spaces.js +101 -0
- package/clis/hf/top.js +1 -0
- package/clis/homebrew/cask.js +39 -0
- package/clis/homebrew/formula.js +41 -0
- package/clis/homebrew/popular.js +54 -0
- package/clis/homebrew/utils.js +100 -0
- package/clis/hupu/__fixtures__/hot-home.html +64 -0
- package/clis/hupu/detail.js +0 -1
- package/clis/hupu/hot.js +156 -35
- package/clis/hupu/hot.test.js +224 -0
- package/clis/hupu/search.js +0 -1
- package/clis/instagram/note.js +1 -1
- package/clis/instagram/note.test.js +1 -29
- package/clis/instagram/post.js +1 -1
- package/clis/instagram/post.test.js +1 -1
- package/clis/instagram/reel.js +1 -1
- package/clis/instagram/story.js +1 -1
- package/clis/instagram/story.test.js +1 -34
- package/clis/jd/commands.test.js +1 -24
- package/clis/lichess/lichess.test.js +85 -0
- package/clis/lichess/top.js +46 -0
- package/clis/lichess/user.js +91 -0
- package/clis/lichess/utils.js +97 -0
- package/clis/linkedin/search.js +107 -10
- package/clis/linkedin/search.test.js +222 -0
- package/clis/linux-do/feed.js +2 -5
- package/clis/linux-do/feed.test.js +35 -0
- package/clis/lobsters/domain.js +92 -0
- package/clis/maven/artifact.js +49 -0
- package/clis/maven/search.js +51 -0
- package/clis/maven/utils.js +110 -0
- package/clis/mdn/search.js +97 -0
- package/clis/medium/tag.js +135 -0
- package/clis/npm/downloads.js +59 -0
- package/clis/npm/package.js +70 -0
- package/clis/npm/search.js +49 -0
- package/clis/npm/utils.js +76 -0
- package/clis/nuget/nuget.test.js +111 -0
- package/clis/nuget/package.js +101 -0
- package/clis/nuget/search.js +69 -0
- package/clis/nuget/utils.js +87 -0
- package/clis/nvd/cve.js +121 -0
- package/clis/oeis/oeis.test.js +88 -0
- package/clis/oeis/search.js +63 -0
- package/clis/oeis/sequence.js +71 -0
- package/clis/oeis/utils.js +88 -0
- package/clis/openalex/search.js +69 -0
- package/clis/openalex/utils.js +160 -0
- package/clis/openalex/work.js +65 -0
- package/clis/openfda/drug-label.js +74 -0
- package/clis/openfda/food-recall.js +65 -0
- package/clis/openfda/openfda.test.js +114 -0
- package/clis/openfda/utils.js +67 -0
- package/clis/osv/osv.test.js +97 -0
- package/clis/osv/query.js +72 -0
- package/clis/osv/utils.js +169 -0
- package/clis/osv/vulnerability.js +54 -0
- package/clis/packagist/package.js +49 -0
- package/clis/packagist/search.js +43 -0
- package/clis/packagist/utils.js +113 -0
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/pixiv/download.test.js +1 -1
- package/clis/pixiv/illusts.test.js +1 -1
- package/clis/pixiv/search.test.js +1 -1
- package/clis/pubmed/article.js +50 -0
- package/clis/pubmed/author.js +64 -0
- package/clis/pubmed/citations.js +36 -0
- package/clis/pubmed/pubmed.test.js +276 -0
- package/clis/pubmed/related.js +45 -0
- package/clis/pubmed/search.js +75 -0
- package/clis/pubmed/utils.js +309 -0
- package/clis/pypi/downloads.js +66 -0
- package/clis/pypi/package.js +79 -0
- package/clis/pypi/utils.js +55 -0
- package/clis/quark/mv.js +1 -1
- package/clis/quark/save.js +1 -1
- package/clis/qwen/ask.js +85 -0
- package/clis/qwen/detail.js +62 -0
- package/clis/qwen/history.js +61 -0
- package/clis/qwen/image.js +179 -0
- package/clis/qwen/new.js +23 -0
- package/clis/qwen/read.js +41 -0
- package/clis/qwen/send.js +55 -0
- package/clis/qwen/status.js +37 -0
- package/clis/qwen/utils.js +409 -0
- package/clis/qwen/utils.test.js +45 -0
- package/clis/rest-countries/country.js +65 -0
- package/clis/rest-countries/region.js +64 -0
- package/clis/rest-countries/rest-countries.test.js +83 -0
- package/clis/rest-countries/utils.js +126 -0
- package/clis/reuters/article-detail.js +53 -0
- package/clis/reuters/reuters.test.js +299 -0
- package/clis/reuters/search.js +45 -34
- package/clis/reuters/utils.js +159 -0
- package/clis/rfc/rfc.js +52 -0
- package/clis/rfc/rfc.test.js +74 -0
- package/clis/rfc/utils.js +72 -0
- package/clis/rubygems/gem.js +42 -0
- package/clis/rubygems/search.js +47 -0
- package/clis/rubygems/utils.js +86 -0
- package/clis/stackoverflow/related.js +66 -0
- package/clis/stackoverflow/stackoverflow.test.js +58 -0
- package/clis/stackoverflow/tag.js +60 -0
- package/clis/stackoverflow/user.js +50 -0
- package/clis/stackoverflow/utils.js +118 -0
- package/clis/steam/app.js +67 -0
- package/clis/steam/search.js +58 -0
- package/clis/steam/steam.test.js +46 -0
- package/clis/steam/utils.js +107 -0
- package/clis/taobao/commands.test.js +1 -24
- package/clis/test-utils.js +61 -0
- package/clis/tieba/hot.js +0 -1
- package/clis/tiktok/comment.js +128 -41
- package/clis/tiktok/creator-videos.js +270 -0
- package/clis/tiktok/creator-videos.test.js +113 -0
- package/clis/tiktok/explore.js +137 -29
- package/clis/tiktok/follow.js +115 -33
- package/clis/tiktok/following.js +157 -36
- package/clis/tiktok/friends.js +139 -37
- package/clis/tiktok/live.js +137 -41
- package/clis/tiktok/notifications.js +141 -38
- package/clis/tiktok/refactor.test.js +389 -0
- package/clis/tiktok/unfollow.js +124 -38
- package/clis/tiktok/user.js +203 -29
- package/clis/tiktok/utils.js +505 -0
- package/clis/tiktok/write-refactor.test.js +370 -0
- package/clis/toutiao/articles.js +36 -62
- package/clis/toutiao/hot.js +63 -0
- package/clis/toutiao/toutiao.test.js +378 -0
- package/clis/toutiao/utils.js +161 -0
- package/clis/tvmaze/search.js +61 -0
- package/clis/tvmaze/show.js +60 -0
- package/clis/tvmaze/tvmaze.test.js +93 -0
- package/clis/tvmaze/utils.js +110 -0
- package/clis/twitter/accept.js +1 -1
- package/clis/twitter/followers.js +134 -69
- package/clis/twitter/quote.js +139 -0
- package/clis/twitter/quote.test.js +106 -0
- package/clis/twitter/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- package/clis/twitter/retweet.js +99 -0
- package/clis/twitter/retweet.test.js +69 -0
- package/clis/twitter/shared.js +38 -0
- package/clis/twitter/shared.test.js +28 -1
- package/clis/twitter/unlike.js +87 -0
- package/clis/twitter/unlike.test.js +72 -0
- package/clis/twitter/unretweet.js +99 -0
- package/clis/twitter/unretweet.test.js +69 -0
- package/clis/uisdc/news.js +105 -0
- package/clis/uisdc/news.test.js +66 -0
- package/clis/wanfang/search.js +0 -1
- package/clis/web/read.js +47 -17
- package/clis/web/read.test.js +101 -1
- package/clis/weixin/create-draft.js +1 -1
- package/clis/weixin/drafts.js +1 -1
- package/clis/weixin/drafts.test.js +5 -1
- package/clis/weixin/search.js +157 -0
- package/clis/weixin/search.test.js +227 -0
- package/clis/wikidata/entity.js +60 -0
- package/clis/wikidata/search.js +50 -0
- package/clis/wikidata/utils.js +117 -0
- package/clis/wikidata/wikidata.test.js +83 -0
- package/clis/wikipedia/page.js +95 -0
- package/clis/wttr/current.js +63 -0
- package/clis/wttr/forecast.js +71 -0
- package/clis/wttr/utils.js +50 -0
- package/clis/wttr/wttr.test.js +84 -0
- package/clis/xianyu/chat.js +16 -4
- package/clis/xianyu/chat.test.js +64 -0
- package/clis/xianyu/publish.js +485 -0
- package/clis/xianyu/publish.test.js +220 -0
- package/clis/xiaoe/catalog.js +105 -40
- package/clis/xiaoe/content.js +164 -29
- package/clis/xiaoe/courses.js +86 -29
- package/clis/xiaoe/xiaoe.test.js +486 -0
- package/clis/xiaohongshu/creator-notes-summary.js +1 -1
- package/clis/xiaohongshu/publish.js +16 -3
- package/clis/xiaohongshu/publish.test.js +46 -1
- package/clis/youtube/transcript.js +13 -19
- package/clis/youtube/transcript.test.js +17 -0
- package/clis/yuanbao/ask.js +17 -66
- package/clis/yuanbao/ask.test.js +5 -5
- package/clis/yuanbao/detail.js +65 -0
- package/clis/yuanbao/history.js +51 -0
- package/clis/yuanbao/new.js +1 -0
- package/clis/yuanbao/read.js +38 -0
- package/clis/yuanbao/send.js +57 -0
- package/clis/yuanbao/shared.js +297 -5
- package/clis/yuanbao/shared.test.js +80 -0
- package/clis/yuanbao/status.js +44 -0
- package/clis/zlibrary/commands.test.js +1 -11
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +44 -1
- package/dist/src/browser/base-page.test.js +66 -0
- package/dist/src/browser/bridge.js +47 -45
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +51 -9
- package/dist/src/browser/daemon-client.d.ts +4 -0
- package/dist/src/browser/errors.js +1 -1
- package/dist/src/browser/page.d.ts +1 -1
- package/dist/src/browser/page.js +3 -1
- package/dist/src/browser/page.test.js +29 -0
- package/dist/src/browser/target-errors.d.ts +2 -1
- package/dist/src/browser/target-errors.js +1 -0
- package/dist/src/browser/target-resolver.d.ts +25 -0
- package/dist/src/browser/target-resolver.js +43 -0
- package/dist/src/browser.test.js +18 -0
- package/dist/src/build-manifest.js +9 -4
- package/dist/src/build-manifest.test.js +2 -8
- package/dist/src/capabilityRouting.d.ts +16 -1
- package/dist/src/capabilityRouting.js +24 -1
- package/dist/src/capabilityRouting.test.js +19 -1
- package/dist/src/cli.js +76 -11
- package/dist/src/cli.test.js +241 -1
- package/dist/src/commanderAdapter.js +23 -9
- package/dist/src/commanderAdapter.test.js +0 -1
- package/dist/src/discovery.js +2 -5
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +111 -27
- package/dist/src/execution.test.js +326 -17
- package/dist/src/help.d.ts +27 -2
- package/dist/src/help.js +196 -23
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +54 -0
- package/dist/src/main.js +14 -1
- package/dist/src/manifest-types.d.ts +5 -3
- package/dist/src/pipeline/executor.js +1 -1
- package/dist/src/pipeline/executor.test.js +8 -0
- package/dist/src/pipeline/registry.d.ts +9 -0
- package/dist/src/pipeline/registry.js +13 -1
- package/dist/src/pipeline/steps/browser.d.ts +1 -0
- package/dist/src/pipeline/steps/browser.js +10 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +12 -11
- package/dist/src/registry.js +16 -6
- package/dist/src/registry.test.js +2 -2
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/serialization.d.ts +2 -2
- package/dist/src/serialization.js +4 -6
- package/dist/src/serialization.test.js +17 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/validate.js +15 -11
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +1 -1
- package/scripts/typed-error-lint-baseline.json +5 -77
- package/clis/ctrip/search.test.js +0 -64
- package/clis/gov-policy/commands.test.js +0 -27
- package/clis/linux-do/category.js +0 -37
- package/clis/linux-do/hot.js +0 -26
- package/clis/linux-do/latest.js +0 -19
- package/clis/pixiv/test-utils.js +0 -23
- package/clis/toutiao/articles.test.js +0 -30
- package/dist/src/analysis.d.ts +0 -40
- package/dist/src/analysis.js +0 -172
|
@@ -54,62 +54,64 @@ export class BrowserBridge {
|
|
|
54
54
|
const effectiveSeconds = (timeoutSeconds && timeoutSeconds > 0) ? timeoutSeconds : Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000);
|
|
55
55
|
const timeoutMs = effectiveSeconds * 1000;
|
|
56
56
|
const health = await getDaemonHealth({ contextId });
|
|
57
|
+
// Detect stale daemon before any fast path. A stale daemon can still have
|
|
58
|
+
// the extension connected, so this cannot live only in the no-extension branch.
|
|
59
|
+
const daemonVersion = health.status?.daemonVersion;
|
|
60
|
+
const isStale = !!health.status && (!daemonVersion || daemonVersion !== PKG_VERSION);
|
|
61
|
+
let staleDaemonReplaced = false;
|
|
62
|
+
if (isStale) {
|
|
63
|
+
// Stale daemon — restart it so all browser commands run against the
|
|
64
|
+
// currently installed package code, not the old daemon binary.
|
|
65
|
+
const reason = daemonVersion
|
|
66
|
+
? `v${daemonVersion} ≠ v${PKG_VERSION}`
|
|
67
|
+
: `pre-version daemon, CLI is v${PKG_VERSION}`;
|
|
68
|
+
if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
|
|
69
|
+
process.stderr.write(`⚠️ Stale daemon detected (${reason}). Restarting...\n`);
|
|
70
|
+
}
|
|
71
|
+
const shutdownAccepted = await requestDaemonShutdown();
|
|
72
|
+
const portReleased = shutdownAccepted && await waitForDaemonStop(3000);
|
|
73
|
+
if (!portReleased) {
|
|
74
|
+
// Stale daemon replacement failed — don't blindly spawn on an occupied port
|
|
75
|
+
throw new BrowserConnectError('Stale daemon could not be replaced', `A stale daemon (${reason}) is running but did not shut down.\n` +
|
|
76
|
+
' Run manually: opencli daemon stop && opencli doctor', 'daemon-not-running');
|
|
77
|
+
}
|
|
78
|
+
// Port released — fall through to spawn a fresh daemon
|
|
79
|
+
staleDaemonReplaced = true;
|
|
80
|
+
}
|
|
57
81
|
// Fast path: everything ready
|
|
58
|
-
if (health.state === 'ready')
|
|
82
|
+
if (!staleDaemonReplaced && health.state === 'ready')
|
|
59
83
|
return;
|
|
60
|
-
if (health.state === 'profile-required') {
|
|
84
|
+
if (!staleDaemonReplaced && health.state === 'profile-required') {
|
|
61
85
|
throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
|
|
62
86
|
'Run opencli profile list to see connected profiles.', 'profile-required');
|
|
63
87
|
}
|
|
64
|
-
if (health.state === 'profile-disconnected') {
|
|
88
|
+
if (!staleDaemonReplaced && health.state === 'profile-disconnected') {
|
|
65
89
|
const label = contextId ?? health.status.contextId ?? 'unknown';
|
|
66
90
|
throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
|
|
67
91
|
}
|
|
68
92
|
// Daemon running but no extension
|
|
69
|
-
if (health.state === 'no-extension') {
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Stale daemon — restart it so extension gets a fresh WebSocket endpoint
|
|
75
|
-
const reason = daemonVersion
|
|
76
|
-
? `v${daemonVersion} ≠ v${PKG_VERSION}`
|
|
77
|
-
: `pre-version daemon, CLI is v${PKG_VERSION}`;
|
|
78
|
-
if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
|
|
79
|
-
process.stderr.write(`⚠️ Stale daemon detected (${reason}). Restarting...\n`);
|
|
80
|
-
}
|
|
81
|
-
const shutdownAccepted = await requestDaemonShutdown();
|
|
82
|
-
const portReleased = shutdownAccepted && await waitForDaemonStop(3000);
|
|
83
|
-
if (!portReleased) {
|
|
84
|
-
// Stale daemon replacement failed — don't blindly spawn on an occupied port
|
|
85
|
-
throw new BrowserConnectError('Stale daemon could not be replaced', `A stale daemon (${reason}) is running but did not shut down.\n` +
|
|
86
|
-
' Run manually: opencli daemon stop && opencli doctor', 'daemon-not-running');
|
|
87
|
-
}
|
|
88
|
-
// Port released — fall through to spawn a fresh daemon
|
|
93
|
+
if (!staleDaemonReplaced && health.state === 'no-extension') {
|
|
94
|
+
// Same version — wait for extension to connect
|
|
95
|
+
if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
|
|
96
|
+
process.stderr.write('⏳ Waiting for Chrome/Chromium extension to connect...\n');
|
|
97
|
+
process.stderr.write(' Make sure Chrome or Chromium is open and the OpenCLI extension is enabled.\n');
|
|
89
98
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (await this._pollUntilReady(timeoutMs, contextId))
|
|
97
|
-
return;
|
|
98
|
-
const finalHealth = await getDaemonHealth({ contextId });
|
|
99
|
-
if (finalHealth.state === 'profile-required') {
|
|
100
|
-
throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
|
|
101
|
-
'Run opencli profile list to see connected profiles.', 'profile-required');
|
|
102
|
-
}
|
|
103
|
-
if (finalHealth.state === 'profile-disconnected') {
|
|
104
|
-
const label = contextId ?? finalHealth.status.contextId ?? 'unknown';
|
|
105
|
-
throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
|
|
106
|
-
}
|
|
107
|
-
throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
|
|
108
|
-
'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
|
|
109
|
-
'If not installed:\n' +
|
|
110
|
-
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
111
|
-
' 2. Open chrome://extensions → Developer Mode → Load unpacked', 'extension-not-connected');
|
|
99
|
+
if (await this._pollUntilReady(timeoutMs, contextId))
|
|
100
|
+
return;
|
|
101
|
+
const finalHealth = await getDaemonHealth({ contextId });
|
|
102
|
+
if (finalHealth.state === 'profile-required') {
|
|
103
|
+
throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
|
|
104
|
+
'Run opencli profile list to see connected profiles.', 'profile-required');
|
|
112
105
|
}
|
|
106
|
+
if (finalHealth.state === 'profile-disconnected') {
|
|
107
|
+
const label = contextId ?? finalHealth.status.contextId ?? 'unknown';
|
|
108
|
+
throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
|
|
109
|
+
}
|
|
110
|
+
throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
|
|
111
|
+
'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
|
|
112
|
+
'If not installed:\n' +
|
|
113
|
+
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
114
|
+
' 2. Open chrome://extensions → Developer Mode → Load unpacked', 'extension-not-connected');
|
|
113
115
|
}
|
|
114
116
|
// No daemon — spawn one
|
|
115
117
|
if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
|
|
@@ -26,6 +26,7 @@ export declare class CDPBridge implements IBrowserFactory {
|
|
|
26
26
|
workspace?: string;
|
|
27
27
|
cdpEndpoint?: string;
|
|
28
28
|
contextId?: string;
|
|
29
|
+
idleTimeout?: number;
|
|
29
30
|
}): Promise<IPage>;
|
|
30
31
|
close(): Promise<void>;
|
|
31
32
|
send(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -202,16 +202,58 @@ class CDPPage extends BasePage {
|
|
|
202
202
|
: cookies;
|
|
203
203
|
}
|
|
204
204
|
async screenshot(options = {}) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
205
|
+
const fullPage = options.fullPage === true;
|
|
206
|
+
const overrideWidth = options.width && options.width > 0 ? Math.ceil(options.width) : undefined;
|
|
207
|
+
// height is ignored under fullPage so the captureBeyondViewport path stays unchanged for users who pass --height alongside --full-page.
|
|
208
|
+
const overrideHeight = !fullPage && options.height && options.height > 0 ? Math.ceil(options.height) : undefined;
|
|
209
|
+
const needsOverride = overrideWidth !== undefined || overrideHeight !== undefined;
|
|
210
|
+
if (needsOverride) {
|
|
211
|
+
if (overrideWidth !== undefined && fullPage) {
|
|
212
|
+
await this.bridge.send('Emulation.setDeviceMetricsOverride', {
|
|
213
|
+
mobile: false,
|
|
214
|
+
width: overrideWidth,
|
|
215
|
+
height: 0,
|
|
216
|
+
deviceScaleFactor: 1,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
let finalWidth = overrideWidth ?? 0;
|
|
220
|
+
let finalHeight = overrideHeight ?? 0;
|
|
221
|
+
if (fullPage) {
|
|
222
|
+
const metrics = await this.bridge.send('Page.getLayoutMetrics');
|
|
223
|
+
const m = isRecord(metrics) ? metrics : {};
|
|
224
|
+
const css = isRecord(m.cssContentSize) ? m.cssContentSize : undefined;
|
|
225
|
+
const fb = isRecord(m.contentSize) ? m.contentSize : undefined;
|
|
226
|
+
const size = css ?? fb;
|
|
227
|
+
if (size && typeof size.width === 'number' && typeof size.height === 'number') {
|
|
228
|
+
if (finalWidth === 0)
|
|
229
|
+
finalWidth = Math.ceil(size.width);
|
|
230
|
+
finalHeight = Math.ceil(size.height);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
await this.bridge.send('Emulation.setDeviceMetricsOverride', {
|
|
234
|
+
mobile: false,
|
|
235
|
+
width: finalWidth,
|
|
236
|
+
height: finalHeight,
|
|
237
|
+
deviceScaleFactor: 1,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const result = await this.bridge.send('Page.captureScreenshot', {
|
|
242
|
+
format: options.format ?? 'png',
|
|
243
|
+
quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
|
|
244
|
+
captureBeyondViewport: !needsOverride && fullPage,
|
|
245
|
+
});
|
|
246
|
+
const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
|
|
247
|
+
if (options.path) {
|
|
248
|
+
await saveBase64ToFile(base64, options.path);
|
|
249
|
+
}
|
|
250
|
+
return base64;
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
if (needsOverride) {
|
|
254
|
+
await this.bridge.send('Emulation.clearDeviceMetricsOverride').catch(() => { });
|
|
255
|
+
}
|
|
213
256
|
}
|
|
214
|
-
return base64;
|
|
215
257
|
}
|
|
216
258
|
async startNetworkCapture(pattern = '') {
|
|
217
259
|
// Always update the filter pattern
|
|
@@ -20,6 +20,10 @@ export interface DaemonCommand {
|
|
|
20
20
|
format?: 'png' | 'jpeg';
|
|
21
21
|
quality?: number;
|
|
22
22
|
fullPage?: boolean;
|
|
23
|
+
/** Override viewport width in CSS pixels for screenshot (0 / undefined = use current) */
|
|
24
|
+
width?: number;
|
|
25
|
+
/** Override viewport height in CSS pixels for screenshot (0 / undefined = use current; ignored when fullPage) */
|
|
26
|
+
height?: number;
|
|
23
27
|
/** Local file paths for set-file-input action */
|
|
24
28
|
files?: string[];
|
|
25
29
|
/** CSS selector for file input element (set-file-input action) */
|
|
@@ -65,7 +65,7 @@ export function isTransientBrowserError(err) {
|
|
|
65
65
|
export function formatBrowserConnectError(kind, detail) {
|
|
66
66
|
switch (kind) {
|
|
67
67
|
case 'daemon-not-running':
|
|
68
|
-
return new BrowserConnectError('Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''), `
|
|
68
|
+
return new BrowserConnectError('Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''), `Run \`opencli doctor\` to diagnose, or \`opencli daemon restart\` to force a fresh daemon. Default port is ${DEFAULT_DAEMON_PORT}.`, kind);
|
|
69
69
|
case 'extension-not-connected':
|
|
70
70
|
return new BrowserConnectError('Browser Bridge extension is not connected.' + (detail ? `\n\n${detail}` : ''), 'Install the extension from GitHub Releases, then reload.', kind);
|
|
71
71
|
case 'command-failed':
|
|
@@ -41,7 +41,7 @@ export declare class Page extends BasePage {
|
|
|
41
41
|
domain?: string;
|
|
42
42
|
url?: string;
|
|
43
43
|
}): Promise<BrowserCookie[]>;
|
|
44
|
-
/**
|
|
44
|
+
/** Release the current automation tab lease in the extension */
|
|
45
45
|
closeWindow(): Promise<void>;
|
|
46
46
|
tabs(): Promise<unknown[]>;
|
|
47
47
|
newTab(url?: string): Promise<string | undefined>;
|
package/dist/src/browser/page.js
CHANGED
|
@@ -143,7 +143,7 @@ export class Page extends BasePage {
|
|
|
143
143
|
const result = await sendCommand('cookies', { ...this._wsOpt(), ...opts });
|
|
144
144
|
return Array.isArray(result) ? result : [];
|
|
145
145
|
}
|
|
146
|
-
/**
|
|
146
|
+
/** Release the current automation tab lease in the extension */
|
|
147
147
|
async closeWindow() {
|
|
148
148
|
try {
|
|
149
149
|
await sendCommand('close-window', { ...this._wsOpt() });
|
|
@@ -205,6 +205,8 @@ export class Page extends BasePage {
|
|
|
205
205
|
format: options.format,
|
|
206
206
|
quality: options.quality,
|
|
207
207
|
fullPage: options.fullPage,
|
|
208
|
+
width: options.width,
|
|
209
|
+
height: options.height,
|
|
208
210
|
});
|
|
209
211
|
if (options.path) {
|
|
210
212
|
await saveBase64ToFile(base64, options.path);
|
|
@@ -216,3 +216,32 @@ describe('Page active target tracking', () => {
|
|
|
216
216
|
expect(evalCall?.[1]).not.toHaveProperty('page');
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
+
describe('Page.screenshot', () => {
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
sendCommandMock.mockReset();
|
|
222
|
+
sendCommandFullMock.mockReset();
|
|
223
|
+
warnMock.mockReset();
|
|
224
|
+
});
|
|
225
|
+
it('forwards width / height / fullPage options to the bridge', async () => {
|
|
226
|
+
sendCommandMock.mockResolvedValueOnce('BASE64');
|
|
227
|
+
const page = new Page('browser:default');
|
|
228
|
+
const data = await page.screenshot({ fullPage: true, width: 1080 });
|
|
229
|
+
expect(data).toBe('BASE64');
|
|
230
|
+
expect(sendCommandMock).toHaveBeenCalledWith('screenshot', expect.objectContaining({
|
|
231
|
+
workspace: 'browser:default',
|
|
232
|
+
fullPage: true,
|
|
233
|
+
width: 1080,
|
|
234
|
+
}));
|
|
235
|
+
});
|
|
236
|
+
it('omits viewport overrides when none are set', async () => {
|
|
237
|
+
sendCommandMock.mockResolvedValueOnce('BASE64');
|
|
238
|
+
const page = new Page('browser:default');
|
|
239
|
+
await page.screenshot();
|
|
240
|
+
const call = sendCommandMock.mock.calls.at(-1);
|
|
241
|
+
expect(call?.[0]).toBe('screenshot');
|
|
242
|
+
const args = call?.[1];
|
|
243
|
+
expect(args.width).toBeUndefined();
|
|
244
|
+
expect(args.height).toBeUndefined();
|
|
245
|
+
expect(args.fullPage).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* - selector_not_found: 0 matches
|
|
16
16
|
* - selector_ambiguous: >1 matches for a write op without --nth
|
|
17
17
|
* - selector_nth_out_of_range: --nth beyond matches_n
|
|
18
|
+
* - not_editable: target exists but cannot accept text input
|
|
18
19
|
*/
|
|
19
|
-
export type TargetErrorCode = 'not_found' | 'stale_ref' | 'invalid_selector' | 'selector_not_found' | 'selector_ambiguous' | 'selector_nth_out_of_range';
|
|
20
|
+
export type TargetErrorCode = 'not_found' | 'stale_ref' | 'invalid_selector' | 'selector_not_found' | 'selector_ambiguous' | 'selector_nth_out_of_range' | 'not_editable';
|
|
20
21
|
export interface TargetErrorInfo {
|
|
21
22
|
code: TargetErrorCode;
|
|
22
23
|
message: string;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - selector_not_found: 0 matches
|
|
16
16
|
* - selector_ambiguous: >1 matches for a write op without --nth
|
|
17
17
|
* - selector_nth_out_of_range: --nth beyond matches_n
|
|
18
|
+
* - not_editable: target exists but cannot accept text input
|
|
18
19
|
*/
|
|
19
20
|
export class TargetError extends Error {
|
|
20
21
|
code;
|
|
@@ -79,6 +79,25 @@ export declare function clickResolvedJs(opts?: {
|
|
|
79
79
|
* Generate JS for type that uses the unified resolver.
|
|
80
80
|
*/
|
|
81
81
|
export declare function typeResolvedJs(text: string): string;
|
|
82
|
+
export type FillResolvedResult = {
|
|
83
|
+
ok: true;
|
|
84
|
+
actual: string;
|
|
85
|
+
expected: string;
|
|
86
|
+
length: number;
|
|
87
|
+
mode: 'input' | 'textarea' | 'contenteditable';
|
|
88
|
+
} | {
|
|
89
|
+
ok: false;
|
|
90
|
+
actual: string;
|
|
91
|
+
expected: string;
|
|
92
|
+
length: number;
|
|
93
|
+
mode: 'input' | 'textarea' | 'contenteditable';
|
|
94
|
+
} | {
|
|
95
|
+
ok: false;
|
|
96
|
+
reason: string;
|
|
97
|
+
tag?: string;
|
|
98
|
+
type?: string;
|
|
99
|
+
role?: string;
|
|
100
|
+
};
|
|
82
101
|
/**
|
|
83
102
|
* Prepare the resolved element for native CDP Input.insertText.
|
|
84
103
|
*
|
|
@@ -90,6 +109,12 @@ export declare function prepareNativeTypeResolvedJs(opts?: {
|
|
|
90
109
|
skipScroll?: boolean;
|
|
91
110
|
skipFocus?: boolean;
|
|
92
111
|
}): string;
|
|
112
|
+
/**
|
|
113
|
+
* Verify the exact value/text currently held by the resolved editable target.
|
|
114
|
+
* Assumes resolveTargetJs and prepareNativeTypeResolvedJs have already set
|
|
115
|
+
* `window.__resolved` to the normalized editable host.
|
|
116
|
+
*/
|
|
117
|
+
export declare function verifyFilledResolvedJs(expected: string): string;
|
|
93
118
|
/**
|
|
94
119
|
* Generate JS for scrollTo that uses the unified resolver.
|
|
95
120
|
* Assumes resolveTargetJs has been called and __resolved is set.
|
|
@@ -411,6 +411,49 @@ export function prepareNativeTypeResolvedJs(opts = {}) {
|
|
|
411
411
|
})()
|
|
412
412
|
`;
|
|
413
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Verify the exact value/text currently held by the resolved editable target.
|
|
416
|
+
* Assumes resolveTargetJs and prepareNativeTypeResolvedJs have already set
|
|
417
|
+
* `window.__resolved` to the normalized editable host.
|
|
418
|
+
*/
|
|
419
|
+
export function verifyFilledResolvedJs(expected) {
|
|
420
|
+
const safeText = JSON.stringify(expected);
|
|
421
|
+
return `
|
|
422
|
+
(() => {
|
|
423
|
+
const el = window.__resolved;
|
|
424
|
+
if (!el) return { ok: false, reason: 'no_resolved_element' };
|
|
425
|
+
|
|
426
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
427
|
+
const isInput = el instanceof HTMLInputElement;
|
|
428
|
+
const isTextarea = el instanceof HTMLTextAreaElement;
|
|
429
|
+
const mode = el.isContentEditable
|
|
430
|
+
? 'contenteditable'
|
|
431
|
+
: isTextarea
|
|
432
|
+
? 'textarea'
|
|
433
|
+
: isInput
|
|
434
|
+
? 'input'
|
|
435
|
+
: '';
|
|
436
|
+
|
|
437
|
+
if (!mode) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
reason: 'not_editable',
|
|
441
|
+
tag,
|
|
442
|
+
role: el.getAttribute ? (el.getAttribute('role') || '') : '',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const actual = mode === 'contenteditable' ? (el.innerText || '') : String(el.value || '');
|
|
447
|
+
return {
|
|
448
|
+
ok: actual === ${safeText},
|
|
449
|
+
actual,
|
|
450
|
+
expected: ${safeText},
|
|
451
|
+
length: actual.length,
|
|
452
|
+
mode,
|
|
453
|
+
};
|
|
454
|
+
})()
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
414
457
|
/**
|
|
415
458
|
* Generate JS for scrollTo that uses the unified resolver.
|
|
416
459
|
* Assumes resolveTargetJs has been called and __resolved is set.
|
package/dist/src/browser.test.js
CHANGED
|
@@ -165,6 +165,24 @@ describe('BrowserBridge state', () => {
|
|
|
165
165
|
const bridge = new BrowserBridge();
|
|
166
166
|
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Stale daemon could not be replaced');
|
|
167
167
|
});
|
|
168
|
+
it('attempts stale daemon replacement even when extension is connected', async () => {
|
|
169
|
+
vi.spyOn(daemonClient, 'getDaemonHealth').mockResolvedValue({
|
|
170
|
+
state: 'ready',
|
|
171
|
+
status: {
|
|
172
|
+
ok: true,
|
|
173
|
+
pid: 1,
|
|
174
|
+
uptime: 0,
|
|
175
|
+
daemonVersion: '0.0.1',
|
|
176
|
+
extensionConnected: true,
|
|
177
|
+
pending: 0,
|
|
178
|
+
memoryMB: 0,
|
|
179
|
+
port: 0,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
vi.spyOn(daemonClient, 'requestDaemonShutdown').mockResolvedValue(false);
|
|
183
|
+
const bridge = new BrowserBridge();
|
|
184
|
+
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Stale daemon could not be replaced');
|
|
185
|
+
});
|
|
168
186
|
});
|
|
169
187
|
describe('stealth anti-detection', () => {
|
|
170
188
|
it('generates non-empty JS string', () => {
|
|
@@ -32,7 +32,13 @@ const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
|
|
|
32
32
|
const CLIS_DIR = path.join(PACKAGE_ROOT, 'clis');
|
|
33
33
|
// Write manifest next to clis/ so both dev and installed runtime can find it.
|
|
34
34
|
const OUTPUT = getCliManifestPath(CLIS_DIR);
|
|
35
|
-
|
|
35
|
+
// Module is treated as a CLI command source if it either:
|
|
36
|
+
// 1. Calls `cli(...)` directly (the common case), or
|
|
37
|
+
// 2. Calls a factory `make<Pascal>Command(...)` from clis/_shared/ that
|
|
38
|
+
// wraps `cli(...)`. Without (2), shared-factory adapters
|
|
39
|
+
// (codex/cursor/chatwise new/status/dump/screenshot) match no `cli(`
|
|
40
|
+
// token at the top level and silently drop out of the manifest.
|
|
41
|
+
const CLI_MODULE_PATTERN = /\bcli\s*\(|\bmake[A-Z]\w*Command\s*\(/;
|
|
36
42
|
/**
|
|
37
43
|
* Thrown by `loadManifestEntries` when an adapter file looks like a CLI
|
|
38
44
|
* module (matches CLI_MODULE_PATTERN) but cannot be imported. Callers
|
|
@@ -91,13 +97,12 @@ function toManifestEntry(cmd, modulePath, sourceFile) {
|
|
|
91
97
|
browser: cmd.browser ?? true,
|
|
92
98
|
args: toManifestArgs(cmd.args),
|
|
93
99
|
columns: cmd.columns,
|
|
94
|
-
|
|
95
|
-
deprecated: cmd.deprecated,
|
|
96
|
-
replacedBy: cmd.replacedBy,
|
|
100
|
+
defaultFormat: cmd.defaultFormat,
|
|
97
101
|
type: 'js',
|
|
98
102
|
modulePath,
|
|
99
103
|
sourceFile,
|
|
100
104
|
navigateBefore: cmd.navigateBefore,
|
|
105
|
+
browserSession: cmd.browserSession,
|
|
101
106
|
};
|
|
102
107
|
}
|
|
103
108
|
/**
|
|
@@ -46,8 +46,7 @@ describe('manifest helper rules', () => {
|
|
|
46
46
|
],
|
|
47
47
|
domain: 'localhost',
|
|
48
48
|
navigateBefore: 'https://example.com/session',
|
|
49
|
-
|
|
50
|
-
replacedBy: 'opencli demo new',
|
|
49
|
+
defaultFormat: 'plain',
|
|
51
50
|
}),
|
|
52
51
|
}));
|
|
53
52
|
expect(entries).toMatchObject([
|
|
@@ -74,8 +73,7 @@ describe('manifest helper rules', () => {
|
|
|
74
73
|
type: 'js',
|
|
75
74
|
modulePath: `${site}/${site}.js`,
|
|
76
75
|
navigateBefore: 'https://example.com/session',
|
|
77
|
-
|
|
78
|
-
replacedBy: 'opencli demo new',
|
|
76
|
+
defaultFormat: 'plain',
|
|
79
77
|
},
|
|
80
78
|
]);
|
|
81
79
|
// Verify sourceFile is included and stable for manifest consumers.
|
|
@@ -96,8 +94,6 @@ describe('manifest helper rules', () => {
|
|
|
96
94
|
name: 'legacy',
|
|
97
95
|
access: 'read',
|
|
98
96
|
description: 'legacy command',
|
|
99
|
-
deprecated: 'legacy is deprecated',
|
|
100
|
-
replacedBy: 'opencli demo new',
|
|
101
97
|
});
|
|
102
98
|
return {};
|
|
103
99
|
});
|
|
@@ -112,8 +108,6 @@ describe('manifest helper rules', () => {
|
|
|
112
108
|
args: [],
|
|
113
109
|
type: 'js',
|
|
114
110
|
modulePath: `${site}/${site}.js`,
|
|
115
|
-
deprecated: 'legacy is deprecated',
|
|
116
|
-
replacedBy: 'opencli demo new',
|
|
117
111
|
},
|
|
118
112
|
]);
|
|
119
113
|
// Verify sourceFile is included
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { CliCommand } from './registry.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Pipeline steps that require a live browser session.
|
|
4
|
+
*
|
|
5
|
+
* Note: this is the *subset* of registered pipeline steps that need a page;
|
|
6
|
+
* non-browser steps (fetch, select, map, filter, sort, limit, download)
|
|
7
|
+
* deliberately stay out. The full registered-step list lives in
|
|
8
|
+
* `src/pipeline/registry.ts` and is re-derived elsewhere via
|
|
9
|
+
* `getRegisteredStepNames()` (e.g. in `validate.ts`). When you add a new
|
|
10
|
+
* pipeline step, decide whether it belongs here based on whether its handler
|
|
11
|
+
* touches the IPage object — and `src/capabilityRouting.test.ts` verifies the
|
|
12
|
+
* subset relationship is intact.
|
|
13
|
+
*/
|
|
3
14
|
export declare const BROWSER_ONLY_STEPS: Set<string>;
|
|
15
|
+
/** Internal helper: ensure BROWSER_ONLY_STEPS is a subset of registered pipeline steps. */
|
|
16
|
+
export declare function _validateBrowserOnlyStepsAgainstRegistry(): {
|
|
17
|
+
extras: string[];
|
|
18
|
+
};
|
|
4
19
|
export declare function shouldUseBrowserSession(cmd: CliCommand): boolean;
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import { getRegisteredStepNames } from './pipeline/registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pipeline steps that require a live browser session.
|
|
4
|
+
*
|
|
5
|
+
* Note: this is the *subset* of registered pipeline steps that need a page;
|
|
6
|
+
* non-browser steps (fetch, select, map, filter, sort, limit, download)
|
|
7
|
+
* deliberately stay out. The full registered-step list lives in
|
|
8
|
+
* `src/pipeline/registry.ts` and is re-derived elsewhere via
|
|
9
|
+
* `getRegisteredStepNames()` (e.g. in `validate.ts`). When you add a new
|
|
10
|
+
* pipeline step, decide whether it belongs here based on whether its handler
|
|
11
|
+
* touches the IPage object — and `src/capabilityRouting.test.ts` verifies the
|
|
12
|
+
* subset relationship is intact.
|
|
13
|
+
*/
|
|
2
14
|
export const BROWSER_ONLY_STEPS = new Set([
|
|
3
15
|
'navigate',
|
|
4
16
|
'click',
|
|
5
17
|
'type',
|
|
18
|
+
'fill',
|
|
6
19
|
'wait',
|
|
7
20
|
'press',
|
|
8
21
|
'snapshot',
|
|
@@ -10,6 +23,16 @@ export const BROWSER_ONLY_STEPS = new Set([
|
|
|
10
23
|
'intercept',
|
|
11
24
|
'tap',
|
|
12
25
|
]);
|
|
26
|
+
/** Internal helper: ensure BROWSER_ONLY_STEPS is a subset of registered pipeline steps. */
|
|
27
|
+
export function _validateBrowserOnlyStepsAgainstRegistry() {
|
|
28
|
+
const registered = new Set(getRegisteredStepNames());
|
|
29
|
+
const extras = [];
|
|
30
|
+
for (const step of BROWSER_ONLY_STEPS) {
|
|
31
|
+
if (!registered.has(step))
|
|
32
|
+
extras.push(step);
|
|
33
|
+
}
|
|
34
|
+
return { extras };
|
|
35
|
+
}
|
|
13
36
|
function pipelineNeedsBrowserSession(pipeline) {
|
|
14
37
|
return pipeline.some((step) => {
|
|
15
38
|
if (!step || typeof step !== 'object')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Strategy } from './registry.js';
|
|
3
|
-
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
3
|
+
import { BROWSER_ONLY_STEPS, _validateBrowserOnlyStepsAgainstRegistry, shouldUseBrowserSession } from './capabilityRouting.js';
|
|
4
|
+
import { getRegisteredStepNames } from './pipeline/registry.js';
|
|
4
5
|
function makeCmd(partial) {
|
|
5
6
|
return {
|
|
6
7
|
site: 'test',
|
|
@@ -42,4 +43,21 @@ describe('shouldUseBrowserSession', () => {
|
|
|
42
43
|
func: async () => [],
|
|
43
44
|
}))).toBe(true);
|
|
44
45
|
});
|
|
46
|
+
it('routes pipelines containing the fill step into a browser session', () => {
|
|
47
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
48
|
+
browser: true,
|
|
49
|
+
strategy: Strategy.PUBLIC,
|
|
50
|
+
pipeline: [{ navigate: 'https://example.com' }, { fill: { ref: '#q', text: 'hello' } }],
|
|
51
|
+
}))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('BROWSER_ONLY_STEPS / pipeline registry alignment', () => {
|
|
55
|
+
it('is a subset of registered pipeline step names', () => {
|
|
56
|
+
const { extras } = _validateBrowserOnlyStepsAgainstRegistry();
|
|
57
|
+
expect(extras).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('includes fill (DOM-touching step added in PR #1222)', () => {
|
|
60
|
+
expect(BROWSER_ONLY_STEPS.has('fill')).toBe(true);
|
|
61
|
+
expect(getRegisteredStepNames()).toContain('fill');
|
|
62
|
+
});
|
|
45
63
|
});
|