@jackwener/opencli 1.7.12 → 1.7.13
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 +12194 -6843
- 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/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- 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/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/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 +150 -0
- package/dist/src/commanderAdapter.js +0 -5
- 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 +23 -2
- package/dist/src/help.js +41 -19
- 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
|
@@ -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,8 +7,29 @@ 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 rootHelpData(program: Command, groups: RootAdapterGroups): Record<string, unknown>;
|
|
12
33
|
export declare function siteHelpData(site: string, commands: readonly CliCommand[]): Record<string, unknown>;
|
|
13
34
|
export declare function commandHelpData(cmd: CliCommand): Record<string, unknown>;
|
|
14
35
|
export declare function installStructuredHelp(command: Command, data: () => unknown, textSuffix?: string | (() => string)): void;
|
package/dist/src/help.js
CHANGED
|
@@ -50,18 +50,32 @@ export function wrapCommaList(items, opts = {}) {
|
|
|
50
50
|
lines.push(line);
|
|
51
51
|
return lines.join('\n');
|
|
52
52
|
}
|
|
53
|
-
export function
|
|
54
|
-
if (
|
|
55
|
-
return '';
|
|
53
|
+
export function classifyAdapter(domain) {
|
|
54
|
+
if (!domain)
|
|
55
|
+
return 'site';
|
|
56
|
+
return domain.includes('.') ? 'site' : 'app';
|
|
57
|
+
}
|
|
58
|
+
function formatGroupSection(label, names) {
|
|
59
|
+
if (names.length === 0)
|
|
60
|
+
return [];
|
|
56
61
|
return [
|
|
62
|
+
`${label} (${names.length}):`,
|
|
63
|
+
wrapCommaList(names),
|
|
57
64
|
'',
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
''
|
|
64
|
-
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
export function formatRootAdapterHelpText(groups) {
|
|
68
|
+
const total = groups.external.length + groups.apps.length + groups.sites.length;
|
|
69
|
+
if (total === 0)
|
|
70
|
+
return '';
|
|
71
|
+
const lines = [''];
|
|
72
|
+
lines.push(...formatGroupSection('External CLIs', groups.external));
|
|
73
|
+
lines.push(...formatGroupSection('App adapters', groups.apps));
|
|
74
|
+
lines.push(...formatGroupSection('Site adapters', groups.sites));
|
|
75
|
+
lines.push("Run 'opencli list' for full command details, or 'opencli <site> --help' to inspect one site.");
|
|
76
|
+
lines.push("Agent tip: use 'opencli <site> --help -f yaml' for structured commands, args, access, and examples.");
|
|
77
|
+
lines.push('');
|
|
78
|
+
return lines.join('\n');
|
|
65
79
|
}
|
|
66
80
|
function compactArg(arg) {
|
|
67
81
|
return {
|
|
@@ -84,26 +98,35 @@ function compactCommand(cmd, opts = {}) {
|
|
|
84
98
|
...(cmd.aliases?.length ? { aliases: cmd.aliases } : {}),
|
|
85
99
|
args: cmd.args.map(compactArg),
|
|
86
100
|
example: formatCommandExample(cmd),
|
|
101
|
+
...(cmd.browserSession ? { browserSession: cmd.browserSession } : {}),
|
|
102
|
+
...(cmd.defaultFormat ? { defaultFormat: cmd.defaultFormat } : {}),
|
|
87
103
|
...(opts.includeColumns && cmd.columns?.length ? { columns: cmd.columns } : {}),
|
|
88
|
-
...(cmd.deprecated ? { deprecated: cmd.deprecated } : {}),
|
|
89
|
-
...(cmd.replacedBy ? { replacedBy: cmd.replacedBy } : {}),
|
|
90
104
|
};
|
|
91
105
|
}
|
|
92
|
-
export function rootHelpData(program,
|
|
93
|
-
const
|
|
106
|
+
export function rootHelpData(program, groups) {
|
|
107
|
+
const adapterNames = new Set([...groups.external, ...groups.apps, ...groups.sites]);
|
|
94
108
|
const commands = program.commands
|
|
95
|
-
.filter(command => !
|
|
109
|
+
.filter(command => !adapterNames.has(command.name()))
|
|
96
110
|
.map(command => ({
|
|
97
111
|
name: command.name(),
|
|
98
112
|
description: command.description(),
|
|
99
113
|
}));
|
|
114
|
+
const sortLocale = (a, b) => a.localeCompare(b);
|
|
100
115
|
return {
|
|
101
116
|
name: program.name(),
|
|
102
117
|
description: program.description(),
|
|
103
118
|
commands,
|
|
119
|
+
external_clis: {
|
|
120
|
+
count: groups.external.length,
|
|
121
|
+
clis: [...groups.external].sort(sortLocale),
|
|
122
|
+
},
|
|
123
|
+
app_adapters: {
|
|
124
|
+
count: groups.apps.length,
|
|
125
|
+
apps: [...groups.apps].sort(sortLocale),
|
|
126
|
+
},
|
|
104
127
|
site_adapters: {
|
|
105
|
-
count:
|
|
106
|
-
sites: [...
|
|
128
|
+
count: groups.sites.length,
|
|
129
|
+
sites: [...groups.sites].sort(sortLocale),
|
|
107
130
|
},
|
|
108
131
|
next: [
|
|
109
132
|
'opencli <site> --help -f yaml',
|
|
@@ -144,6 +167,5 @@ export function installStructuredHelp(command, data, textSuffix) {
|
|
|
144
167
|
}
|
|
145
168
|
export function formatSiteCommandDescription(cmd) {
|
|
146
169
|
const access = cmd.access === 'write' ? '[write]' : '[read]';
|
|
147
|
-
|
|
148
|
-
return `${access} ${cmd.description}${deprecatedSuffix}`;
|
|
170
|
+
return `${access} ${cmd.description}`;
|
|
149
171
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { classifyAdapter, formatRootAdapterHelpText } from './help.js';
|
|
3
|
+
describe('classifyAdapter', () => {
|
|
4
|
+
it('classifies DNS-style domains as site', () => {
|
|
5
|
+
expect(classifyAdapter('www.bilibili.com')).toBe('site');
|
|
6
|
+
expect(classifyAdapter('chatgpt.com')).toBe('site');
|
|
7
|
+
expect(classifyAdapter('claude.ai')).toBe('site');
|
|
8
|
+
expect(classifyAdapter('grok.com')).toBe('site');
|
|
9
|
+
});
|
|
10
|
+
it('classifies localhost as app (Electron / osascript desktop integrations)', () => {
|
|
11
|
+
expect(classifyAdapter('localhost')).toBe('app');
|
|
12
|
+
});
|
|
13
|
+
it('classifies non-DNS domain strings as app (e.g. literal "doubao-app")', () => {
|
|
14
|
+
expect(classifyAdapter('doubao-app')).toBe('app');
|
|
15
|
+
});
|
|
16
|
+
it('defaults missing domain to site (most adapters without explicit domain are public web scrapers)', () => {
|
|
17
|
+
expect(classifyAdapter(undefined)).toBe('site');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('formatRootAdapterHelpText', () => {
|
|
21
|
+
it('renders all three sections in External / App / Site order when populated', () => {
|
|
22
|
+
const text = formatRootAdapterHelpText({
|
|
23
|
+
external: ['gh', 'docker'],
|
|
24
|
+
apps: ['chatwise', 'codex'],
|
|
25
|
+
sites: ['bilibili'],
|
|
26
|
+
});
|
|
27
|
+
expect(text).toContain('External CLIs (2):');
|
|
28
|
+
expect(text).toContain('App adapters (2):');
|
|
29
|
+
expect(text).toContain('Site adapters (1):');
|
|
30
|
+
expect(text.indexOf('External CLIs')).toBeLessThan(text.indexOf('App adapters'));
|
|
31
|
+
expect(text.indexOf('App adapters')).toBeLessThan(text.indexOf('Site adapters'));
|
|
32
|
+
});
|
|
33
|
+
it('omits empty sections instead of rendering a (0) header', () => {
|
|
34
|
+
const text = formatRootAdapterHelpText({
|
|
35
|
+
external: [],
|
|
36
|
+
apps: [],
|
|
37
|
+
sites: ['bilibili'],
|
|
38
|
+
});
|
|
39
|
+
expect(text).not.toContain('External CLIs');
|
|
40
|
+
expect(text).not.toContain('App adapters');
|
|
41
|
+
expect(text).toContain('Site adapters (1):');
|
|
42
|
+
});
|
|
43
|
+
it('returns empty string when all groups are empty', () => {
|
|
44
|
+
expect(formatRootAdapterHelpText({ external: [], apps: [], sites: [] })).toBe('');
|
|
45
|
+
});
|
|
46
|
+
it('always renders the agent discovery hint when any section is populated', () => {
|
|
47
|
+
const text = formatRootAdapterHelpText({
|
|
48
|
+
external: [],
|
|
49
|
+
apps: [],
|
|
50
|
+
sites: ['bilibili'],
|
|
51
|
+
});
|
|
52
|
+
expect(text).toContain("'opencli <site> --help -f yaml'");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/dist/src/main.js
CHANGED
|
@@ -28,7 +28,7 @@ const __dirname = path.dirname(__filename);
|
|
|
28
28
|
const BUILTIN_CLIS = path.join(findPackageRoot(__filename), 'clis');
|
|
29
29
|
const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
30
30
|
// ── Session lifecycle flags ──────────────────────────────────────────────
|
|
31
|
-
// `--live` / `--focus` are top-level-ish toggles that tweak the automation
|
|
31
|
+
// `--live` / `--focus` / `--reuse` are top-level-ish toggles that tweak the automation
|
|
32
32
|
// window's lifecycle. We strip them from argv before Commander runs so they
|
|
33
33
|
// can be placed anywhere and work on any subcommand (adapter or browser).
|
|
34
34
|
{
|
|
@@ -42,6 +42,19 @@ const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
|
42
42
|
process.env.OPENCLI_WINDOW_FOCUSED = '1';
|
|
43
43
|
process.argv.splice(focusIdx, 1);
|
|
44
44
|
}
|
|
45
|
+
const reuseIdx = process.argv.findIndex(arg => arg === '--reuse' || arg.startsWith('--reuse='));
|
|
46
|
+
if (reuseIdx !== -1) {
|
|
47
|
+
const arg = process.argv[reuseIdx];
|
|
48
|
+
const value = arg.startsWith('--reuse=')
|
|
49
|
+
? arg.slice('--reuse='.length)
|
|
50
|
+
: process.argv[reuseIdx + 1];
|
|
51
|
+
if (value !== 'none' && value !== 'site') {
|
|
52
|
+
process.stderr.write(`--reuse must be one of: none, site. Received: "${value ?? ''}"\n`);
|
|
53
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
54
|
+
}
|
|
55
|
+
process.env.OPENCLI_BROWSER_REUSE = value;
|
|
56
|
+
process.argv.splice(reuseIdx, arg.startsWith('--reuse=') ? 1 : 2);
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
// ── Ultra-fast path: lightweight commands bypass full discovery ──────────
|
|
47
60
|
// These are high-frequency or trivial paths that must not pay the startup tax.
|
|
@@ -28,9 +28,7 @@ export interface ManifestEntry {
|
|
|
28
28
|
}>;
|
|
29
29
|
columns?: string[];
|
|
30
30
|
pipeline?: Record<string, unknown>[];
|
|
31
|
-
|
|
32
|
-
deprecated?: boolean | string;
|
|
33
|
-
replacedBy?: string;
|
|
31
|
+
defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
|
|
34
32
|
type: 'js';
|
|
35
33
|
/** Relative path from clis/ dir, e.g. 'bilibili/search.js' */
|
|
36
34
|
modulePath?: string;
|
|
@@ -38,4 +36,8 @@ export interface ManifestEntry {
|
|
|
38
36
|
sourceFile?: string;
|
|
39
37
|
/** Pre-navigation control — see CliCommand.navigateBefore */
|
|
40
38
|
navigateBefore?: boolean | string;
|
|
39
|
+
/** Browser session lifecycle defaults — see CliCommand.browserSession */
|
|
40
|
+
browserSession?: {
|
|
41
|
+
reuse?: 'none' | 'site';
|
|
42
|
+
};
|
|
41
43
|
}
|
|
@@ -32,7 +32,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
catch (err) {
|
|
35
|
-
// Attempt cleanup:
|
|
35
|
+
// Attempt cleanup: release automation tab lease on pipeline failure.
|
|
36
36
|
if (page?.closeWindow) {
|
|
37
37
|
try {
|
|
38
38
|
await page.closeWindow();
|
|
@@ -14,6 +14,7 @@ function createMockPage(overrides = {}) {
|
|
|
14
14
|
snapshot: vi.fn().mockResolvedValue(''),
|
|
15
15
|
click: vi.fn(),
|
|
16
16
|
typeText: vi.fn(),
|
|
17
|
+
fillText: vi.fn(),
|
|
17
18
|
pressKey: vi.fn(),
|
|
18
19
|
getFormState: vi.fn().mockResolvedValue({}),
|
|
19
20
|
wait: vi.fn(),
|
|
@@ -159,6 +160,13 @@ describe('executePipeline', () => {
|
|
|
159
160
|
]);
|
|
160
161
|
expect(page.click).toHaveBeenCalledWith('5');
|
|
161
162
|
});
|
|
163
|
+
it('fill step calls page.fillText with raw rendered text', async () => {
|
|
164
|
+
const page = createMockPage();
|
|
165
|
+
await executePipeline(page, [
|
|
166
|
+
{ fill: { ref: '@5', text: 'line1\\n/ / ${{ args.tail }}' } },
|
|
167
|
+
], { args: { tail: 'raw' } });
|
|
168
|
+
expect(page.fillText).toHaveBeenCalledWith('5', 'line1\\n/ / raw');
|
|
169
|
+
});
|
|
162
170
|
it('navigate preserves existing data through pipeline', async () => {
|
|
163
171
|
const page = createMockPage({
|
|
164
172
|
evaluate: vi.fn().mockResolvedValue([{ a: 1 }]),
|
|
@@ -13,6 +13,15 @@ export type StepHandler<TData = unknown, TResult = unknown, TParams = unknown> =
|
|
|
13
13
|
* Get a registered step handler by name.
|
|
14
14
|
*/
|
|
15
15
|
export declare function getStep(name: string): StepHandler | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* List all currently registered step names. Used by `validate.ts` to allowlist
|
|
18
|
+
* step names without maintaining a parallel hand-coded list.
|
|
19
|
+
*
|
|
20
|
+
* Note: this depends on registerStep() side effects below already having run.
|
|
21
|
+
* Importing this module triggers all core registrations at the bottom of the
|
|
22
|
+
* file, so the returned array reflects every core + plugin step at call time.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getRegisteredStepNames(): string[];
|
|
16
25
|
/**
|
|
17
26
|
* Register a new custom step handler for the YAML pipeline.
|
|
18
27
|
*/
|