@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
package/dist/src/execution.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* This is the single entry point for executing any CLI command. It handles:
|
|
5
5
|
* 1. Argument validation and coercion
|
|
6
6
|
* 2. Browser session lifecycle (if needed)
|
|
7
|
-
* 3. Domain pre-navigation for cookie
|
|
7
|
+
* 3. Domain pre-navigation for cookie strategies
|
|
8
8
|
* 4. Timeout enforcement
|
|
9
9
|
* 5. Lazy-loading of TS modules from manifest
|
|
10
10
|
* 6. Lifecycle hooks (onBeforeExecute / onAfterExecute)
|
|
11
11
|
*/
|
|
12
12
|
import { getRegistry, fullName, } from './registry.js';
|
|
13
13
|
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import * as crypto from 'node:crypto';
|
|
14
15
|
import * as fs from 'node:fs';
|
|
15
16
|
import * as os from 'node:os';
|
|
16
17
|
import { executePipeline } from './pipeline/index.js';
|
|
@@ -28,6 +29,7 @@ const _loadedModules = new Map();
|
|
|
28
29
|
/** Track mtime of loaded user adapter files for hot-reload in daemon mode. */
|
|
29
30
|
const _moduleMtimes = new Map();
|
|
30
31
|
const _userClisDir = `${os.homedir()}/.opencli/clis/`;
|
|
32
|
+
const INTERACTIVE_BROWSER_IDLE_TIMEOUT_SECONDS = 600;
|
|
31
33
|
function normalizeTraceMode(raw) {
|
|
32
34
|
if (raw === undefined || raw === null || raw === '' || raw === 'off')
|
|
33
35
|
return 'off';
|
|
@@ -138,14 +140,37 @@ function resolvePreNav(cmd) {
|
|
|
138
140
|
// strategy → navigateBefore expansion already happened in normalizeCommand().
|
|
139
141
|
return null;
|
|
140
142
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
function urlMatchesDomain(url, domain) {
|
|
144
|
+
if (!url || !domain)
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
const hostname = new URL(url).hostname;
|
|
148
|
+
return hostname === domain || hostname.endsWith(`.${domain}`);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function isDomainRootPreNav(preNavUrl, domain) {
|
|
155
|
+
if (!domain)
|
|
156
|
+
return false;
|
|
157
|
+
try {
|
|
158
|
+
const parsed = new URL(preNavUrl);
|
|
159
|
+
const hostnameMatches = parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
|
|
160
|
+
const rootPath = parsed.pathname === '' || parsed.pathname === '/';
|
|
161
|
+
return hostnameMatches && rootPath && parsed.search === '' && parsed.hash === '';
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function shouldRunPreNav(cmd, page, reuse, preNavUrl) {
|
|
168
|
+
if (reuse !== 'site' || !cmd.domain)
|
|
169
|
+
return true;
|
|
170
|
+
if (!isDomainRootPreNav(preNavUrl, cmd.domain))
|
|
171
|
+
return true;
|
|
172
|
+
const currentUrl = await page.getCurrentUrl?.().catch(() => null);
|
|
173
|
+
return !urlMatchesDomain(currentUrl, cmd.domain);
|
|
149
174
|
}
|
|
150
175
|
export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
151
176
|
let kwargs;
|
|
@@ -157,6 +182,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
157
182
|
throw err;
|
|
158
183
|
throw new ArgumentError(getErrorMessage(err));
|
|
159
184
|
}
|
|
185
|
+
const userTimeoutSec = readUserTimeoutSeconds(cmd, kwargs);
|
|
160
186
|
const traceMode = normalizeTraceMode(opts.trace);
|
|
161
187
|
const hookCtx = {
|
|
162
188
|
command: fullName(cmd),
|
|
@@ -183,17 +209,19 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
183
209
|
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
|
|
184
210
|
}
|
|
185
211
|
}
|
|
186
|
-
ensureRequiredEnv(cmd);
|
|
187
212
|
const BrowserFactory = getBrowserFactory(cmd.site);
|
|
188
213
|
const contextId = resolveProfileContextId(opts.profile);
|
|
189
214
|
const internal = cmd;
|
|
215
|
+
const browserReuse = resolveBrowserSessionReuse(cmd);
|
|
216
|
+
const workspace = resolveBrowserWorkspace(cmd, browserReuse);
|
|
217
|
+
const idleTimeout = browserReuse === 'site' ? INTERACTIVE_BROWSER_IDLE_TIMEOUT_SECONDS : undefined;
|
|
190
218
|
result = await browserSession(BrowserFactory, async (page) => {
|
|
191
219
|
const observation = traceMode === 'off'
|
|
192
220
|
? null
|
|
193
221
|
: new ObservationSession({
|
|
194
222
|
scope: {
|
|
195
223
|
contextId,
|
|
196
|
-
workspace
|
|
224
|
+
workspace,
|
|
197
225
|
target: page.getActivePage?.(),
|
|
198
226
|
site: cmd.site,
|
|
199
227
|
command: fullName(cmd),
|
|
@@ -210,7 +238,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
210
238
|
await page.startNetworkCapture?.().catch(() => false);
|
|
211
239
|
}
|
|
212
240
|
const preNavUrl = resolvePreNav(cmd);
|
|
213
|
-
if (preNavUrl) {
|
|
241
|
+
if (preNavUrl && await shouldRunPreNav(cmd, page, browserReuse, preNavUrl)) {
|
|
214
242
|
observation?.record({
|
|
215
243
|
stream: 'action',
|
|
216
244
|
name: 'pre_navigate',
|
|
@@ -253,12 +281,15 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
253
281
|
throw wrapped;
|
|
254
282
|
}
|
|
255
283
|
}
|
|
256
|
-
// --live / OPENCLI_LIVE=1 keeps the automation
|
|
257
|
-
// command finishes, so agents (or humans) can inspect the page state.
|
|
258
|
-
const keepOpen = process.env.OPENCLI_LIVE === '1' || process.env.OPENCLI_LIVE === 'true';
|
|
284
|
+
// --live / OPENCLI_LIVE=1 keeps the current automation tab lease after
|
|
285
|
+
// the command finishes, so agents (or humans) can inspect the page state.
|
|
286
|
+
const keepOpen = browserReuse !== 'none' || process.env.OPENCLI_LIVE === '1' || process.env.OPENCLI_LIVE === 'true';
|
|
259
287
|
try {
|
|
288
|
+
const browserTimeout = userTimeoutSec !== null
|
|
289
|
+
? userTimeoutSec + RUNTIME_TIMEOUT_PADDING_SECONDS
|
|
290
|
+
: DEFAULT_BROWSER_COMMAND_TIMEOUT;
|
|
260
291
|
const result = await runWithTimeout(runCommand(cmd, page, kwargs, debug), {
|
|
261
|
-
timeout:
|
|
292
|
+
timeout: browserTimeout,
|
|
262
293
|
label: fullName(cmd),
|
|
263
294
|
});
|
|
264
295
|
observation?.record({
|
|
@@ -270,8 +301,9 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
270
301
|
await collectObservationEvidence(observation, page).catch(() => { });
|
|
271
302
|
exportTraceArtifact(observation, 'success', undefined, opts.onTraceExport);
|
|
272
303
|
}
|
|
273
|
-
// Adapter commands are one-shot —
|
|
274
|
-
// instead of waiting for the 30s idle timeout.
|
|
304
|
+
// Adapter commands are one-shot — release the current tab lease immediately
|
|
305
|
+
// instead of waiting for the 30s idle timeout. The automation container
|
|
306
|
+
// window stays open for reuse.
|
|
275
307
|
if (!keepOpen)
|
|
276
308
|
await page.closeWindow?.().catch(() => { });
|
|
277
309
|
return result;
|
|
@@ -294,23 +326,26 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
294
326
|
exportTraceArtifact(observation, 'failure', err, opts.onTraceExport);
|
|
295
327
|
}
|
|
296
328
|
}
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
329
|
+
// Release the tab lease on failure too — without this, the lease lingers
|
|
330
|
+
// until the extension's idle timer fires (unreliable on Windows where
|
|
331
|
+
// MV3 service workers may be suspended before setTimeout triggers).
|
|
300
332
|
if (!keepOpen)
|
|
301
333
|
await page.closeWindow?.().catch(() => { });
|
|
302
334
|
throw err;
|
|
303
335
|
}
|
|
304
|
-
}, { workspace
|
|
336
|
+
}, { workspace, cdpEndpoint, contextId, idleTimeout });
|
|
305
337
|
}
|
|
306
338
|
else {
|
|
307
|
-
// Non-browser commands:
|
|
308
|
-
|
|
309
|
-
|
|
339
|
+
// Non-browser commands: enforce a timeout only when the command exposes
|
|
340
|
+
// a `--timeout` arg (and the resolved value is positive). Without that
|
|
341
|
+
// arg there is no meaningful default — non-browser cmds are diverse
|
|
342
|
+
// enough that a hard cap would do more harm than good.
|
|
343
|
+
if (userTimeoutSec !== null) {
|
|
344
|
+
const ceiling = userTimeoutSec + RUNTIME_TIMEOUT_PADDING_SECONDS;
|
|
310
345
|
result = await runWithTimeout(runCommand(cmd, null, kwargs, debug), {
|
|
311
|
-
timeout,
|
|
346
|
+
timeout: ceiling,
|
|
312
347
|
label: fullName(cmd),
|
|
313
|
-
hint: `
|
|
348
|
+
hint: `Pass a higher --timeout value (currently ${userTimeoutSec}s)`,
|
|
314
349
|
});
|
|
315
350
|
}
|
|
316
351
|
else {
|
|
@@ -401,3 +436,52 @@ export function prepareCommandArgs(cmd, rawKwargs) {
|
|
|
401
436
|
cmd.validateArgs?.(kwargs);
|
|
402
437
|
return kwargs;
|
|
403
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Runtime ceiling padding (seconds) added on top of the user's `--timeout`.
|
|
441
|
+
* The adapter's polling loop typically uses the full user value; the padding
|
|
442
|
+
* gives us room for the adapter to return + closeWindow + trace export before
|
|
443
|
+
* the runtime kills the Promise.
|
|
444
|
+
*/
|
|
445
|
+
const RUNTIME_TIMEOUT_PADDING_SECONDS = 30;
|
|
446
|
+
function readEnvBrowserSessionReuse() {
|
|
447
|
+
const raw = process.env.OPENCLI_BROWSER_REUSE;
|
|
448
|
+
if (raw === undefined || raw === '')
|
|
449
|
+
return null;
|
|
450
|
+
if (raw === 'none' || raw === 'site')
|
|
451
|
+
return raw;
|
|
452
|
+
throw new ArgumentError(`--reuse must be one of: none, site. Received: "${raw}"`);
|
|
453
|
+
}
|
|
454
|
+
function resolveBrowserSessionReuse(cmd) {
|
|
455
|
+
return readEnvBrowserSessionReuse() ?? cmd.browserSession?.reuse ?? 'none';
|
|
456
|
+
}
|
|
457
|
+
function resolveBrowserWorkspace(cmd, reuse) {
|
|
458
|
+
if (reuse === 'site')
|
|
459
|
+
return `site:${cmd.site}`;
|
|
460
|
+
return `site:${cmd.site}:${crypto.randomUUID()}`;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Resolve the user-controllable `--timeout` arg, in seconds.
|
|
464
|
+
*
|
|
465
|
+
* Convention: a command opts into runtime-enforced timeouts by declaring an
|
|
466
|
+
* arg named `timeout`. The arg's `default` flows through `prepareCommandArgs`
|
|
467
|
+
* into `kwargs.timeout`, so by the time runtime enforcement runs, the value
|
|
468
|
+
* is the merged user-supplied-or-default seconds.
|
|
469
|
+
*
|
|
470
|
+
* Returns the parsed positive integer (seconds), or null if the command does
|
|
471
|
+
* not expose a `timeout` arg. Declaring `timeout` opts into runtime timeout
|
|
472
|
+
* enforcement, so invalid values must fail upfront instead of silently
|
|
473
|
+
* disabling the runtime ceiling.
|
|
474
|
+
*/
|
|
475
|
+
function readUserTimeoutSeconds(cmd, kwargs) {
|
|
476
|
+
if (!cmd.args.some(a => a.name === 'timeout'))
|
|
477
|
+
return null;
|
|
478
|
+
const raw = kwargs.timeout;
|
|
479
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
480
|
+
throw new ArgumentError(`Argument "timeout" must be a positive integer. Received: "${String(raw)}"`);
|
|
481
|
+
}
|
|
482
|
+
const parsed = Number(raw);
|
|
483
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
484
|
+
throw new ArgumentError(`Argument "timeout" must be a positive integer. Received: "${String(raw)}"`);
|
|
485
|
+
}
|
|
486
|
+
return parsed;
|
|
487
|
+
}
|
|
@@ -3,44 +3,353 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { executeCommand, prepareCommandArgs } from './execution.js';
|
|
6
|
-
import { TimeoutError, toEnvelope } from './errors.js';
|
|
6
|
+
import { ArgumentError, TimeoutError, toEnvelope } from './errors.js';
|
|
7
7
|
import { cli, Strategy } from './registry.js';
|
|
8
|
-
import { withTimeoutMs } from './runtime.js';
|
|
9
8
|
import * as runtime from './runtime.js';
|
|
10
9
|
import * as capRouting from './capabilityRouting.js';
|
|
11
10
|
describe('executeCommand — non-browser timeout', () => {
|
|
12
|
-
it('applies
|
|
11
|
+
it('applies the user --timeout arg as the ceiling for non-browser commands', async () => {
|
|
12
|
+
const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
|
|
13
13
|
const cmd = cli({
|
|
14
14
|
site: 'test-execution',
|
|
15
15
|
name: 'non-browser-timeout', access: 'read',
|
|
16
|
-
description: 'test non-browser timeout',
|
|
16
|
+
description: 'test non-browser --timeout enforcement',
|
|
17
17
|
browser: false,
|
|
18
18
|
strategy: Strategy.PUBLIC,
|
|
19
|
-
|
|
19
|
+
args: [
|
|
20
|
+
{ name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
|
|
21
|
+
],
|
|
22
|
+
func: async () => [{ ok: true }],
|
|
23
|
+
});
|
|
24
|
+
await executeCommand(cmd, {});
|
|
25
|
+
expect(runWithTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
26
|
+
// Ceiling = user-supplied/default timeout + 30s padding (adapter return room).
|
|
27
|
+
expect(runWithTimeoutSpy.mock.calls[0]?.[1]).toMatchObject({
|
|
28
|
+
timeout: 35,
|
|
29
|
+
label: 'test-execution/non-browser-timeout',
|
|
30
|
+
});
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
it('fires a TimeoutError when the inner adapter exceeds the --timeout ceiling', async () => {
|
|
34
|
+
const cmd = cli({
|
|
35
|
+
site: 'test-execution',
|
|
36
|
+
name: 'non-browser-timeout-fires', access: 'read',
|
|
37
|
+
description: 'test that the ceiling actually cancels the adapter',
|
|
38
|
+
browser: false,
|
|
39
|
+
strategy: Strategy.PUBLIC,
|
|
40
|
+
args: [
|
|
41
|
+
{ name: 'timeout', type: 'int', required: false, default: 1, help: 'Max seconds' },
|
|
42
|
+
],
|
|
20
43
|
func: () => new Promise(() => { }),
|
|
21
44
|
});
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
// Spy on runWithTimeout to intercept and pass a tiny ceiling so the test
|
|
46
|
+
// doesn't have to wait the real (1+30)s. We still verify the TimeoutError
|
|
47
|
+
// surface — code, label, hint — that users see.
|
|
48
|
+
vi.spyOn(runtime, 'runWithTimeout').mockImplementation(async (promise, opts) => {
|
|
49
|
+
return runtime.withTimeoutMs(promise, 50, () => new TimeoutError(opts.label ?? 'op', opts.timeout, opts.hint));
|
|
50
|
+
});
|
|
51
|
+
const error = await executeCommand(cmd, {}).catch((err) => err);
|
|
26
52
|
expect(error).toBeInstanceOf(TimeoutError);
|
|
27
53
|
expect(error).toMatchObject({
|
|
28
54
|
code: 'TIMEOUT',
|
|
29
|
-
|
|
55
|
+
hint: 'Pass a higher --timeout value (currently 1s)',
|
|
30
56
|
});
|
|
57
|
+
vi.restoreAllMocks();
|
|
31
58
|
});
|
|
32
|
-
it('
|
|
59
|
+
it('runs non-browser commands without a ceiling when no --timeout arg is declared', async () => {
|
|
60
|
+
const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
|
|
33
61
|
const cmd = cli({
|
|
34
62
|
site: 'test-execution',
|
|
35
|
-
name: 'non-browser-
|
|
36
|
-
description: 'test
|
|
63
|
+
name: 'non-browser-no-timeout', access: 'read',
|
|
64
|
+
description: 'test that omitting --timeout means no ceiling',
|
|
37
65
|
browser: false,
|
|
38
66
|
strategy: Strategy.PUBLIC,
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
func: async () => [{ ok: true }],
|
|
68
|
+
});
|
|
69
|
+
await executeCommand(cmd, {});
|
|
70
|
+
expect(runWithTimeoutSpy).not.toHaveBeenCalled();
|
|
71
|
+
vi.restoreAllMocks();
|
|
72
|
+
});
|
|
73
|
+
it('rejects invalid --timeout values instead of silently disabling the non-browser ceiling', async () => {
|
|
74
|
+
const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
|
|
75
|
+
const cmd = cli({
|
|
76
|
+
site: 'test-execution',
|
|
77
|
+
name: 'non-browser-invalid-timeout', access: 'read',
|
|
78
|
+
description: 'test invalid --timeout fails upfront',
|
|
79
|
+
browser: false,
|
|
80
|
+
strategy: Strategy.PUBLIC,
|
|
81
|
+
args: [
|
|
82
|
+
{ name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
|
|
83
|
+
],
|
|
84
|
+
func: async () => [{ ok: true }],
|
|
85
|
+
});
|
|
86
|
+
await expect(executeCommand(cmd, { timeout: 0 })).rejects.toBeInstanceOf(ArgumentError);
|
|
87
|
+
await expect(executeCommand(cmd, { timeout: -1 })).rejects.toBeInstanceOf(ArgumentError);
|
|
88
|
+
await expect(executeCommand(cmd, { timeout: 1.5 })).rejects.toBeInstanceOf(ArgumentError);
|
|
89
|
+
expect(runWithTimeoutSpy).not.toHaveBeenCalled();
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
it('applies the user --timeout arg as the ceiling for browser commands (with +30s padding)', async () => {
|
|
93
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
94
|
+
const mockPage = { closeWindow };
|
|
95
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
96
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
|
|
97
|
+
const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
|
|
98
|
+
const cmd = cli({
|
|
99
|
+
site: 'test-execution',
|
|
100
|
+
name: 'browser-with-timeout', access: 'read',
|
|
101
|
+
description: 'test browser --timeout enforcement',
|
|
102
|
+
browser: true,
|
|
103
|
+
strategy: Strategy.PUBLIC,
|
|
104
|
+
args: [
|
|
105
|
+
{ name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
|
|
106
|
+
],
|
|
107
|
+
func: async () => [{ ok: true }],
|
|
108
|
+
});
|
|
109
|
+
await executeCommand(cmd, {});
|
|
110
|
+
expect(runWithTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(runWithTimeoutSpy.mock.calls[0]?.[1]).toMatchObject({
|
|
112
|
+
timeout: 35,
|
|
113
|
+
label: 'test-execution/browser-with-timeout',
|
|
114
|
+
});
|
|
115
|
+
vi.restoreAllMocks();
|
|
116
|
+
});
|
|
117
|
+
it('falls back to DEFAULT_BROWSER_COMMAND_TIMEOUT for browser commands without a --timeout arg', async () => {
|
|
118
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
119
|
+
const mockPage = { closeWindow };
|
|
120
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
121
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
|
|
122
|
+
const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
|
|
123
|
+
const cmd = cli({
|
|
124
|
+
site: 'test-execution',
|
|
125
|
+
name: 'browser-no-timeout', access: 'read',
|
|
126
|
+
description: 'test browser fallback to global default',
|
|
127
|
+
browser: true,
|
|
128
|
+
strategy: Strategy.PUBLIC,
|
|
129
|
+
func: async () => [{ ok: true }],
|
|
130
|
+
});
|
|
131
|
+
await executeCommand(cmd, {});
|
|
132
|
+
expect(runWithTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(runWithTimeoutSpy.mock.calls[0]?.[1]).toMatchObject({
|
|
134
|
+
timeout: runtime.DEFAULT_BROWSER_COMMAND_TIMEOUT,
|
|
135
|
+
label: 'test-execution/browser-no-timeout',
|
|
136
|
+
});
|
|
137
|
+
vi.restoreAllMocks();
|
|
138
|
+
});
|
|
139
|
+
it('reuses a site-scoped browser workspace and keeps the tab lease open', async () => {
|
|
140
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
141
|
+
const mockPage = { closeWindow };
|
|
142
|
+
const sessionOpts = [];
|
|
143
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
144
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
|
|
145
|
+
sessionOpts.push(opts ?? {});
|
|
146
|
+
return fn(mockPage);
|
|
147
|
+
});
|
|
148
|
+
const cmd = cli({
|
|
149
|
+
site: 'test-execution',
|
|
150
|
+
name: 'browser-reuse-site', access: 'read',
|
|
151
|
+
description: 'test site-scoped browser reuse',
|
|
152
|
+
browser: true,
|
|
153
|
+
strategy: Strategy.PUBLIC,
|
|
154
|
+
browserSession: { reuse: 'site' },
|
|
155
|
+
func: async () => [{ ok: true }],
|
|
41
156
|
});
|
|
42
|
-
|
|
43
|
-
await
|
|
157
|
+
await executeCommand(cmd, {});
|
|
158
|
+
await executeCommand(cmd, {});
|
|
159
|
+
expect(sessionOpts).toHaveLength(2);
|
|
160
|
+
expect(sessionOpts[0]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600 });
|
|
161
|
+
expect(sessionOpts[1]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600 });
|
|
162
|
+
expect(closeWindow).not.toHaveBeenCalled();
|
|
163
|
+
vi.restoreAllMocks();
|
|
164
|
+
});
|
|
165
|
+
it('keeps default browser commands on one-shot workspaces', async () => {
|
|
166
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
167
|
+
const mockPage = { closeWindow };
|
|
168
|
+
const sessionOpts = [];
|
|
169
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
170
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
|
|
171
|
+
sessionOpts.push(opts ?? {});
|
|
172
|
+
return fn(mockPage);
|
|
173
|
+
});
|
|
174
|
+
const cmd = cli({
|
|
175
|
+
site: 'test-execution',
|
|
176
|
+
name: 'browser-reuse-default', access: 'read',
|
|
177
|
+
description: 'test default one-shot browser workspace',
|
|
178
|
+
browser: true,
|
|
179
|
+
strategy: Strategy.PUBLIC,
|
|
180
|
+
func: async () => [{ ok: true }],
|
|
181
|
+
});
|
|
182
|
+
await executeCommand(cmd, {});
|
|
183
|
+
await executeCommand(cmd, {});
|
|
184
|
+
expect(sessionOpts).toHaveLength(2);
|
|
185
|
+
expect(sessionOpts[0]?.workspace).toMatch(/^site:test-execution:/);
|
|
186
|
+
expect(sessionOpts[1]?.workspace).toMatch(/^site:test-execution:/);
|
|
187
|
+
expect(sessionOpts[0]?.workspace).not.toBe(sessionOpts[1]?.workspace);
|
|
188
|
+
expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
|
|
189
|
+
expect(sessionOpts[1]?.idleTimeout).toBeUndefined();
|
|
190
|
+
expect(closeWindow).toHaveBeenCalledTimes(2);
|
|
191
|
+
vi.restoreAllMocks();
|
|
192
|
+
});
|
|
193
|
+
it('lets user --reuse none override adapter reuse metadata', async () => {
|
|
194
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
195
|
+
const mockPage = { closeWindow };
|
|
196
|
+
const sessionOpts = [];
|
|
197
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
198
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
|
|
199
|
+
sessionOpts.push(opts ?? {});
|
|
200
|
+
return fn(mockPage);
|
|
201
|
+
});
|
|
202
|
+
const prev = process.env.OPENCLI_BROWSER_REUSE;
|
|
203
|
+
process.env.OPENCLI_BROWSER_REUSE = 'none';
|
|
204
|
+
try {
|
|
205
|
+
const cmd = cli({
|
|
206
|
+
site: 'test-execution',
|
|
207
|
+
name: 'browser-reuse-override-none', access: 'read',
|
|
208
|
+
description: 'test user reuse override',
|
|
209
|
+
browser: true,
|
|
210
|
+
strategy: Strategy.PUBLIC,
|
|
211
|
+
browserSession: { reuse: 'site' },
|
|
212
|
+
func: async () => [{ ok: true }],
|
|
213
|
+
});
|
|
214
|
+
await executeCommand(cmd, {});
|
|
215
|
+
expect(sessionOpts).toHaveLength(1);
|
|
216
|
+
expect(sessionOpts[0]?.workspace).toMatch(/^site:test-execution:/);
|
|
217
|
+
expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
|
|
218
|
+
expect(closeWindow).toHaveBeenCalledTimes(1);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
if (prev === undefined)
|
|
222
|
+
delete process.env.OPENCLI_BROWSER_REUSE;
|
|
223
|
+
else
|
|
224
|
+
process.env.OPENCLI_BROWSER_REUSE = prev;
|
|
225
|
+
vi.restoreAllMocks();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
it('skips repeated domain pre-navigation for site-reused browser sessions', async () => {
|
|
229
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
230
|
+
const goto = vi.fn().mockResolvedValue(undefined);
|
|
231
|
+
const mockPage = {
|
|
232
|
+
closeWindow,
|
|
233
|
+
goto,
|
|
234
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://grok.com/chat/abc'),
|
|
235
|
+
};
|
|
236
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
237
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
|
|
238
|
+
const cmd = cli({
|
|
239
|
+
site: 'test-execution',
|
|
240
|
+
name: 'browser-reuse-skip-prenav', access: 'read',
|
|
241
|
+
description: 'test reused same-domain tabs do not reset conversation state',
|
|
242
|
+
browser: true,
|
|
243
|
+
strategy: Strategy.COOKIE,
|
|
244
|
+
domain: 'grok.com',
|
|
245
|
+
browserSession: { reuse: 'site' },
|
|
246
|
+
func: async () => [{ ok: true }],
|
|
247
|
+
});
|
|
248
|
+
await executeCommand(cmd, {});
|
|
249
|
+
expect(goto).not.toHaveBeenCalled();
|
|
250
|
+
expect(closeWindow).not.toHaveBeenCalled();
|
|
251
|
+
vi.restoreAllMocks();
|
|
252
|
+
});
|
|
253
|
+
it('keeps explicit path pre-navigation for site-reused browser sessions', async () => {
|
|
254
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
255
|
+
const goto = vi.fn().mockResolvedValue(undefined);
|
|
256
|
+
const mockPage = {
|
|
257
|
+
closeWindow,
|
|
258
|
+
goto,
|
|
259
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://example.com/other'),
|
|
260
|
+
};
|
|
261
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
262
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
|
|
263
|
+
const cmd = cli({
|
|
264
|
+
site: 'test-execution',
|
|
265
|
+
name: 'browser-reuse-path-prenav', access: 'read',
|
|
266
|
+
description: 'test explicit path pre-navigation still runs',
|
|
267
|
+
browser: true,
|
|
268
|
+
strategy: Strategy.COOKIE,
|
|
269
|
+
domain: 'example.com',
|
|
270
|
+
navigateBefore: 'https://example.com/dashboard',
|
|
271
|
+
browserSession: { reuse: 'site' },
|
|
272
|
+
func: async () => [{ ok: true }],
|
|
273
|
+
});
|
|
274
|
+
await executeCommand(cmd, {});
|
|
275
|
+
expect(goto).toHaveBeenCalledWith('https://example.com/dashboard');
|
|
276
|
+
expect(closeWindow).not.toHaveBeenCalled();
|
|
277
|
+
vi.restoreAllMocks();
|
|
278
|
+
});
|
|
279
|
+
it('respects navigateBefore=false so adapter range validation fails before browser navigation', async () => {
|
|
280
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
281
|
+
const goto = vi.fn().mockResolvedValue(undefined);
|
|
282
|
+
const mockPage = {
|
|
283
|
+
closeWindow,
|
|
284
|
+
goto,
|
|
285
|
+
getCurrentUrl: vi.fn().mockResolvedValue('about:blank'),
|
|
286
|
+
};
|
|
287
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
288
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
|
|
289
|
+
const cmd = cli({
|
|
290
|
+
site: 'test-execution',
|
|
291
|
+
name: 'browser-invalid-limit-no-prenav', access: 'read',
|
|
292
|
+
description: 'test adapter range validation can fail before pre-nav',
|
|
293
|
+
browser: true,
|
|
294
|
+
strategy: Strategy.COOKIE,
|
|
295
|
+
domain: 'www.facebook.com',
|
|
296
|
+
navigateBefore: false,
|
|
297
|
+
args: [
|
|
298
|
+
{ name: 'limit', type: 'int', required: false, default: 15, help: 'Limit' },
|
|
299
|
+
],
|
|
300
|
+
func: async (_page, args) => {
|
|
301
|
+
const limit = Number(args.limit);
|
|
302
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
303
|
+
throw new ArgumentError('--limit must be a positive integer in [1, 100]');
|
|
304
|
+
}
|
|
305
|
+
return [{ ok: true }];
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
await expect(executeCommand(cmd, { limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
|
|
309
|
+
expect(goto).not.toHaveBeenCalled();
|
|
310
|
+
vi.restoreAllMocks();
|
|
311
|
+
});
|
|
312
|
+
it('rejects invalid --timeout values instead of falling back to the browser default', async () => {
|
|
313
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
314
|
+
const mockPage = { closeWindow };
|
|
315
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
316
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
|
|
317
|
+
const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
|
|
318
|
+
const cmd = cli({
|
|
319
|
+
site: 'test-execution',
|
|
320
|
+
name: 'browser-invalid-timeout', access: 'read',
|
|
321
|
+
description: 'test invalid browser --timeout fails upfront',
|
|
322
|
+
browser: true,
|
|
323
|
+
strategy: Strategy.PUBLIC,
|
|
324
|
+
args: [
|
|
325
|
+
{ name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
|
|
326
|
+
],
|
|
327
|
+
func: async () => [{ ok: true }],
|
|
328
|
+
});
|
|
329
|
+
await expect(executeCommand(cmd, { timeout: 0 })).rejects.toBeInstanceOf(ArgumentError);
|
|
330
|
+
await expect(executeCommand(cmd, { timeout: -1 })).rejects.toBeInstanceOf(ArgumentError);
|
|
331
|
+
await expect(executeCommand(cmd, { timeout: 1.5 })).rejects.toBeInstanceOf(ArgumentError);
|
|
332
|
+
expect(runWithTimeoutSpy).not.toHaveBeenCalled();
|
|
333
|
+
vi.restoreAllMocks();
|
|
334
|
+
});
|
|
335
|
+
it('rejects invalid browser --timeout before opening a session or pre-navigating', async () => {
|
|
336
|
+
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
|
|
337
|
+
const browserSessionSpy = vi.spyOn(runtime, 'browserSession');
|
|
338
|
+
const cmd = cli({
|
|
339
|
+
site: 'test-execution',
|
|
340
|
+
name: 'browser-invalid-timeout-prenav', access: 'read',
|
|
341
|
+
description: 'test invalid browser --timeout fails before session setup',
|
|
342
|
+
browser: true,
|
|
343
|
+
strategy: Strategy.PUBLIC,
|
|
344
|
+
navigateBefore: 'https://example.com/',
|
|
345
|
+
args: [
|
|
346
|
+
{ name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
|
|
347
|
+
],
|
|
348
|
+
func: async () => [{ ok: true }],
|
|
349
|
+
});
|
|
350
|
+
await expect(executeCommand(cmd, { timeout: 0 })).rejects.toBeInstanceOf(ArgumentError);
|
|
351
|
+
expect(browserSessionSpy).not.toHaveBeenCalled();
|
|
352
|
+
vi.restoreAllMocks();
|
|
44
353
|
});
|
|
45
354
|
it('calls closeWindow on browser command failure', async () => {
|
|
46
355
|
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
package/dist/src/help.d.ts
CHANGED
|
@@ -7,9 +7,34 @@ export declare function wrapCommaList(items: readonly string[], opts?: {
|
|
|
7
7
|
width?: number;
|
|
8
8
|
indent?: string;
|
|
9
9
|
}): string;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Adapter category for help-text grouping.
|
|
12
|
+
*
|
|
13
|
+
* - `site`: web site adapter (real DNS-style domain, e.g. `www.bilibili.com`)
|
|
14
|
+
* - `app`: desktop app adapter (Electron/osascript, signaled by `domain: 'localhost'`
|
|
15
|
+
* or other non-DNS string like `'doubao-app'`)
|
|
16
|
+
*
|
|
17
|
+
* Classification is derived from the adapter's `domain` field — no new schema
|
|
18
|
+
* required. Adapters without a `domain` field default to `site` (most are
|
|
19
|
+
* public web scrapers).
|
|
20
|
+
*/
|
|
21
|
+
export type AdapterKind = 'site' | 'app';
|
|
22
|
+
export declare function classifyAdapter(domain: string | undefined): AdapterKind;
|
|
23
|
+
export interface RootAdapterGroups {
|
|
24
|
+
/** Externally-registered CLIs (docker, gh, vercel, ...) — passthrough binaries */
|
|
25
|
+
external: readonly string[];
|
|
26
|
+
/** Desktop-app adapters (chatgpt-app, chatwise, codex, ...) */
|
|
27
|
+
apps: readonly string[];
|
|
28
|
+
/** Web-site adapters (bilibili, dianping, ...) */
|
|
29
|
+
sites: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function formatRootAdapterHelpText(groups: RootAdapterGroups): string;
|
|
32
|
+
export declare function formatCommandListTerm(cmd: CliCommand): string;
|
|
33
|
+
export declare function rootHelpData(program: Command, groups: RootAdapterGroups): Record<string, unknown>;
|
|
12
34
|
export declare function siteHelpData(site: string, commands: readonly CliCommand[]): Record<string, unknown>;
|
|
13
35
|
export declare function commandHelpData(cmd: CliCommand): Record<string, unknown>;
|
|
36
|
+
export declare function formatCommonOptionsHelpText(): string;
|
|
37
|
+
export declare function formatSiteHelpText(site: string, commands: readonly CliCommand[]): string;
|
|
38
|
+
export declare function formatCommandHelpText(cmd: CliCommand): string;
|
|
14
39
|
export declare function installStructuredHelp(command: Command, data: () => unknown, textSuffix?: string | (() => string)): void;
|
|
15
40
|
export declare function formatSiteCommandDescription(cmd: CliCommand): string;
|