@jackwener/opencli 1.6.0 → 1.6.2
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/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -45
- package/README.zh-CN.md +32 -34
- package/autoresearch/browse-tasks.json +18 -20
- package/autoresearch/commands/debug.ts +163 -0
- package/autoresearch/commands/fix.ts +145 -0
- package/autoresearch/commands/plan.ts +88 -0
- package/autoresearch/commands/run.ts +138 -0
- package/autoresearch/config.ts +82 -0
- package/autoresearch/engine.ts +359 -0
- package/autoresearch/eval-all.ts +127 -0
- package/autoresearch/eval-browse.ts +1 -1
- package/autoresearch/eval-publish.ts +238 -0
- package/autoresearch/eval-save.ts +249 -0
- package/autoresearch/eval-skill.ts +14 -8
- package/autoresearch/eval-v2ex.ts +220 -0
- package/autoresearch/eval-zhihu.ts +230 -0
- package/autoresearch/logger.ts +69 -0
- package/autoresearch/presets/combined-reliability.ts +27 -0
- package/autoresearch/presets/index.ts +23 -0
- package/autoresearch/presets/operate-reliability.ts +24 -0
- package/autoresearch/presets/save-reliability.ts +26 -0
- package/autoresearch/presets/skill-quality.ts +20 -0
- package/autoresearch/presets/v2ex-reliability.ts +24 -0
- package/autoresearch/presets/zhihu-reliability.ts +25 -0
- package/autoresearch/publish-tasks.json +345 -0
- package/autoresearch/run-save.sh +11 -0
- package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
- package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
- package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
- package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
- package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
- package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
- package/autoresearch/save-tasks.json +281 -0
- package/autoresearch/v2ex-tasks.json +899 -0
- package/autoresearch/zhihu-tasks.json +848 -0
- package/bun.lock +615 -0
- package/dist/browser/base-page.d.ts +4 -2
- package/dist/browser/base-page.js +37 -4
- package/dist/browser/bridge.js +10 -8
- package/dist/browser/cdp.js +2 -6
- package/dist/browser/daemon-client.d.ts +11 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.d.ts +4 -2
- package/dist/browser/dom-helpers.js +42 -31
- package/dist/browser/dom-snapshot.js +23 -1
- package/dist/browser/page.d.ts +7 -2
- package/dist/browser/page.js +112 -30
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +1 -0
- package/dist/cli-manifest.json +1133 -182
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +48 -7
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +88 -0
- package/dist/clis/1688/item.d.ts +70 -0
- package/dist/clis/1688/item.js +187 -0
- package/dist/clis/1688/item.test.d.ts +1 -0
- package/dist/clis/1688/item.test.js +67 -0
- package/dist/clis/1688/search.d.ts +56 -0
- package/dist/clis/1688/search.js +309 -0
- package/dist/clis/1688/search.test.d.ts +1 -0
- package/dist/clis/1688/search.test.js +75 -0
- package/dist/clis/1688/shared.d.ts +112 -0
- package/dist/clis/1688/shared.js +514 -0
- package/dist/clis/1688/shared.test.d.ts +1 -0
- package/dist/clis/1688/shared.test.js +57 -0
- package/dist/clis/1688/store.d.ts +45 -0
- package/dist/clis/1688/store.js +226 -0
- package/dist/clis/1688/store.test.d.ts +1 -0
- package/dist/clis/1688/store.test.js +62 -0
- package/dist/clis/amazon/bestsellers.d.ts +0 -20
- package/dist/clis/amazon/bestsellers.js +6 -129
- package/dist/clis/amazon/bestsellers.test.js +12 -3
- package/dist/clis/amazon/movers-shakers.d.ts +1 -0
- package/dist/clis/amazon/movers-shakers.js +7 -0
- package/dist/clis/amazon/new-releases.d.ts +1 -0
- package/dist/clis/amazon/new-releases.js +7 -0
- package/dist/clis/amazon/rankings.d.ts +59 -0
- package/dist/clis/amazon/rankings.js +226 -0
- package/dist/clis/amazon/rankings.test.d.ts +1 -0
- package/dist/clis/amazon/rankings.test.js +41 -0
- package/dist/clis/amazon/shared.d.ts +11 -0
- package/dist/clis/amazon/shared.js +121 -11
- package/dist/clis/amazon/shared.test.js +11 -0
- package/dist/clis/bilibili/comments.js +2 -2
- package/dist/clis/bilibili/comments.test.js +3 -2
- package/dist/clis/bilibili/download.js +2 -1
- package/dist/clis/bilibili/subtitle.js +4 -3
- package/dist/clis/bilibili/subtitle.test.js +2 -1
- package/dist/clis/bilibili/utils.d.ts +5 -0
- package/dist/clis/bilibili/utils.js +30 -0
- package/dist/clis/bilibili/utils.test.d.ts +1 -0
- package/dist/clis/bilibili/utils.test.js +17 -0
- package/dist/clis/douban/marks.js +1 -1
- package/dist/clis/douban/subject.yaml +50 -19
- package/dist/clis/doubao/utils.js +32 -12
- package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
- package/dist/clis/douyin/_shared/transcode.test.js +0 -2
- package/dist/clis/douyin/draft.test.js +0 -2
- package/dist/clis/facebook/search.test.js +0 -2
- package/dist/clis/gemini/ask.js +9 -3
- package/dist/clis/gemini/ask.test.d.ts +1 -0
- package/dist/clis/gemini/ask.test.js +100 -0
- package/dist/clis/gemini/reply-state.test.d.ts +1 -0
- package/dist/clis/gemini/reply-state.test.js +641 -0
- package/dist/clis/gemini/utils.d.ts +44 -1
- package/dist/clis/gemini/utils.js +528 -61
- package/dist/clis/gemini/utils.test.js +149 -2
- package/dist/clis/hupu/detail.d.ts +1 -0
- package/dist/clis/hupu/detail.js +72 -0
- package/dist/clis/hupu/hot.yaml +43 -0
- package/dist/clis/hupu/like.d.ts +1 -0
- package/dist/clis/hupu/like.js +75 -0
- package/dist/clis/hupu/reply.d.ts +1 -0
- package/dist/clis/hupu/reply.js +71 -0
- package/dist/clis/hupu/search.d.ts +1 -0
- package/dist/clis/hupu/search.js +59 -0
- package/dist/clis/hupu/unlike.d.ts +1 -0
- package/dist/clis/hupu/unlike.js +75 -0
- package/dist/clis/hupu/utils.d.ts +20 -0
- package/dist/clis/hupu/utils.js +319 -0
- package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
- package/dist/clis/instagram/_shared/private-publish.js +1030 -0
- package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
- package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
- package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
- package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
- package/dist/clis/instagram/_shared/runtime-info.js +81 -0
- package/dist/clis/instagram/note.d.ts +1 -0
- package/dist/clis/instagram/note.js +222 -0
- package/dist/clis/instagram/note.test.d.ts +1 -0
- package/dist/clis/instagram/note.test.js +81 -0
- package/dist/clis/instagram/post.d.ts +4 -0
- package/dist/clis/instagram/post.js +1496 -0
- package/dist/clis/instagram/post.test.d.ts +1 -0
- package/dist/clis/instagram/post.test.js +1647 -0
- package/dist/clis/instagram/reel.d.ts +1 -0
- package/dist/clis/instagram/reel.js +826 -0
- package/dist/clis/instagram/reel.test.d.ts +1 -0
- package/dist/clis/instagram/reel.test.js +167 -0
- package/dist/clis/instagram/story.d.ts +1 -0
- package/dist/clis/instagram/story.js +115 -0
- package/dist/clis/instagram/story.test.d.ts +1 -0
- package/dist/clis/instagram/story.test.js +167 -0
- package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
- package/dist/clis/sinafinance/stock-rank.js +65 -0
- package/dist/clis/substack/utils.test.js +0 -2
- package/dist/clis/twitter/post.js +72 -45
- package/dist/clis/twitter/post.test.d.ts +1 -0
- package/dist/clis/twitter/post.test.js +116 -0
- package/dist/clis/twitter/reply.d.ts +12 -0
- package/dist/clis/twitter/reply.js +257 -35
- package/dist/clis/twitter/reply.test.d.ts +1 -0
- package/dist/clis/twitter/reply.test.js +151 -0
- package/dist/clis/twitter/search.js +67 -5
- package/dist/clis/twitter/search.test.js +83 -5
- package/dist/clis/xianyu/chat.d.ts +7 -0
- package/dist/clis/xianyu/chat.js +146 -0
- package/dist/clis/xianyu/chat.test.d.ts +1 -0
- package/dist/clis/xianyu/chat.test.js +15 -0
- package/dist/clis/xianyu/item.d.ts +7 -0
- package/dist/clis/xianyu/item.js +152 -0
- package/dist/clis/xianyu/item.test.d.ts +1 -0
- package/dist/clis/xianyu/item.test.js +56 -0
- package/dist/clis/xianyu/search.d.ts +10 -0
- package/dist/clis/xianyu/search.js +134 -0
- package/dist/clis/xianyu/search.test.d.ts +1 -0
- package/dist/clis/xianyu/search.test.js +17 -0
- package/dist/clis/xianyu/utils.d.ts +1 -0
- package/dist/clis/xianyu/utils.js +8 -0
- package/dist/clis/xiaoe/catalog.yaml +129 -0
- package/dist/clis/xiaoe/content.yaml +43 -0
- package/dist/clis/xiaoe/courses.yaml +73 -0
- package/dist/clis/xiaoe/detail.yaml +39 -0
- package/dist/clis/xiaoe/play-url.yaml +124 -0
- package/dist/clis/xiaohongshu/comments.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
- package/dist/clis/xiaohongshu/download.test.js +0 -2
- package/dist/clis/xiaohongshu/note.test.js +0 -2
- package/dist/clis/xiaohongshu/publish.test.js +0 -2
- package/dist/clis/xiaohongshu/search.js +29 -20
- package/dist/clis/xiaohongshu/search.test.js +56 -48
- package/dist/clis/yuanbao/ask.d.ts +21 -0
- package/dist/clis/yuanbao/ask.js +427 -0
- package/dist/clis/yuanbao/ask.test.d.ts +1 -0
- package/dist/clis/yuanbao/ask.test.js +124 -0
- package/dist/clis/yuanbao/new.d.ts +1 -0
- package/dist/clis/yuanbao/new.js +70 -0
- package/dist/clis/yuanbao/new.test.d.ts +1 -0
- package/dist/clis/yuanbao/new.test.js +30 -0
- package/dist/clis/yuanbao/shared.d.ts +13 -0
- package/dist/clis/yuanbao/shared.js +49 -0
- package/dist/clis/zhihu/question.js +30 -19
- package/dist/clis/zhihu/question.test.js +34 -16
- package/dist/commanderAdapter.js +8 -4
- package/dist/commanderAdapter.test.js +42 -0
- package/dist/completion.js +3 -1
- package/dist/completion.test.d.ts +1 -0
- package/dist/completion.test.js +23 -0
- package/dist/doctor.js +1 -1
- package/dist/electron-apps.d.ts +2 -0
- package/dist/electron-apps.js +7 -1
- package/dist/errors.js +1 -1
- package/dist/execution.js +25 -35
- package/dist/explore.js +1 -1
- package/dist/launcher.d.ts +4 -0
- package/dist/launcher.js +64 -8
- package/dist/launcher.test.js +88 -7
- package/dist/output.d.ts +2 -0
- package/dist/output.js +10 -1
- package/dist/output.test.d.ts +0 -3
- package/dist/output.test.js +59 -92
- package/dist/pipeline/executor.test.js +0 -2
- package/dist/pipeline/steps/download.test.js +0 -2
- package/dist/registry.d.ts +2 -0
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +1 -0
- package/dist/types.d.ts +9 -2
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/1688.md +52 -0
- package/docs/adapters/browser/36kr.md +2 -1
- package/docs/adapters/browser/doubao.md +5 -1
- package/docs/adapters/browser/hupu.md +53 -0
- package/docs/adapters/browser/sinafinance.md +32 -2
- package/docs/adapters/browser/weibo.md +6 -1
- package/docs/adapters/browser/wikipedia.md +2 -0
- package/docs/adapters/browser/xianyu.md +42 -0
- package/docs/adapters/browser/xiaoe.md +44 -0
- package/docs/adapters/browser/yuanbao.md +64 -0
- package/docs/adapters/index.md +14 -5
- package/docs/comparison.md +1 -1
- package/docs/developer/ai-workflow.md +2 -2
- package/docs/developer/contributing.md +1 -1
- package/docs/developer/testing.md +2 -0
- package/docs/guide/plugins.md +1 -0
- package/docs/guide/troubleshooting.md +11 -0
- package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
- package/docs/zh/guide/plugins.md +1 -0
- package/extension/dist/background.js +1127 -0
- package/extension/src/background.test.ts +39 -0
- package/extension/src/background.ts +223 -34
- package/extension/src/cdp.ts +194 -4
- package/extension/src/protocol.ts +22 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +1 -1
- package/skills/opencli-explorer/SKILL.md +1 -1
- package/skills/opencli-oneshot/SKILL.md +2 -2
- package/skills/opencli-operate/SKILL.md +120 -27
- package/skills/opencli-usage/SKILL.md +31 -20
- package/skills/opencli-usage/browser.md +114 -16
- package/skills/opencli-usage/public-api.md +32 -3
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +50 -0
- package/skills/smart-search/references/sources-other.md +42 -0
- package/skills/smart-search/references/sources-shopping.md +31 -0
- package/skills/smart-search/references/sources-social.md +51 -0
- package/skills/smart-search/references/sources-tech.md +42 -0
- package/skills/smart-search/references/sources-travel.md +20 -0
- package/src/browser/base-page.ts +41 -6
- package/src/browser/bridge.ts +11 -8
- package/src/browser/cdp.ts +1 -8
- package/src/browser/daemon-client.ts +11 -1
- package/src/browser/dom-helpers.ts +43 -31
- package/src/browser/dom-snapshot.ts +23 -1
- package/src/browser/page.ts +115 -31
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.ts +2 -0
- package/src/cli.test.ts +133 -0
- package/src/cli.ts +73 -11
- package/src/clis/1688/item.test.ts +69 -0
- package/src/clis/1688/item.ts +282 -0
- package/src/clis/1688/search.test.ts +81 -0
- package/src/clis/1688/search.ts +402 -0
- package/src/clis/1688/shared.test.ts +75 -0
- package/src/clis/1688/shared.ts +623 -0
- package/src/clis/1688/store.test.ts +69 -0
- package/src/clis/1688/store.ts +300 -0
- package/src/clis/amazon/bestsellers.test.ts +12 -3
- package/src/clis/amazon/bestsellers.ts +6 -178
- package/src/clis/amazon/movers-shakers.ts +8 -0
- package/src/clis/amazon/new-releases.ts +8 -0
- package/src/clis/amazon/rankings.test.ts +47 -0
- package/src/clis/amazon/rankings.ts +312 -0
- package/src/clis/amazon/shared.test.ts +16 -0
- package/src/clis/amazon/shared.ts +134 -12
- package/src/clis/bilibili/comments.test.ts +4 -3
- package/src/clis/bilibili/comments.ts +2 -2
- package/src/clis/bilibili/download.ts +2 -1
- package/src/clis/bilibili/subtitle.test.ts +2 -1
- package/src/clis/bilibili/subtitle.ts +4 -3
- package/src/clis/bilibili/utils.test.ts +21 -0
- package/src/clis/bilibili/utils.ts +27 -0
- package/src/clis/douban/marks.ts +1 -1
- package/src/clis/douban/subject.yaml +50 -19
- package/src/clis/doubao/utils.ts +32 -12
- package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
- package/src/clis/douyin/_shared/transcode.test.ts +0 -2
- package/src/clis/douyin/draft.test.ts +0 -2
- package/src/clis/facebook/search.test.ts +0 -2
- package/src/clis/gemini/ask.test.ts +116 -0
- package/src/clis/gemini/ask.ts +10 -3
- package/src/clis/gemini/reply-state.test.ts +708 -0
- package/src/clis/gemini/utils.test.ts +184 -2
- package/src/clis/gemini/utils.ts +588 -60
- package/src/clis/hupu/detail.ts +126 -0
- package/src/clis/hupu/hot.yaml +43 -0
- package/src/clis/hupu/like.ts +76 -0
- package/src/clis/hupu/reply.ts +76 -0
- package/src/clis/hupu/search.ts +95 -0
- package/src/clis/hupu/unlike.ts +76 -0
- package/src/clis/hupu/utils.ts +381 -0
- package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
- package/src/clis/instagram/_shared/private-publish.ts +1303 -0
- package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
- package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
- package/src/clis/instagram/_shared/runtime-info.ts +91 -0
- package/src/clis/instagram/note.test.ts +96 -0
- package/src/clis/instagram/note.ts +254 -0
- package/src/clis/instagram/post.test.ts +1716 -0
- package/src/clis/instagram/post.ts +1620 -0
- package/src/clis/instagram/reel.test.ts +191 -0
- package/src/clis/instagram/reel.ts +886 -0
- package/src/clis/instagram/story.test.ts +191 -0
- package/src/clis/instagram/story.ts +151 -0
- package/src/clis/sinafinance/stock-rank.ts +68 -0
- package/src/clis/substack/utils.test.ts +0 -2
- package/src/clis/twitter/post.test.ts +157 -0
- package/src/clis/twitter/post.ts +82 -48
- package/src/clis/twitter/reply.test.ts +177 -0
- package/src/clis/twitter/reply.ts +285 -39
- package/src/clis/twitter/search.test.ts +88 -5
- package/src/clis/twitter/search.ts +68 -5
- package/src/clis/xianyu/chat.test.ts +20 -0
- package/src/clis/xianyu/chat.ts +175 -0
- package/src/clis/xianyu/item.test.ts +67 -0
- package/src/clis/xianyu/item.ts +172 -0
- package/src/clis/xianyu/search.test.ts +22 -0
- package/src/clis/xianyu/search.ts +151 -0
- package/src/clis/xianyu/utils.ts +9 -0
- package/src/clis/xiaoe/catalog.yaml +129 -0
- package/src/clis/xiaoe/content.yaml +43 -0
- package/src/clis/xiaoe/courses.yaml +73 -0
- package/src/clis/xiaoe/detail.yaml +39 -0
- package/src/clis/xiaoe/play-url.yaml +124 -0
- package/src/clis/xiaohongshu/comments.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
- package/src/clis/xiaohongshu/download.test.ts +0 -2
- package/src/clis/xiaohongshu/note.test.ts +0 -2
- package/src/clis/xiaohongshu/publish.test.ts +0 -2
- package/src/clis/xiaohongshu/search.test.ts +59 -48
- package/src/clis/xiaohongshu/search.ts +31 -21
- package/src/clis/yuanbao/ask.test.ts +156 -0
- package/src/clis/yuanbao/ask.ts +522 -0
- package/src/clis/yuanbao/new.test.ts +36 -0
- package/src/clis/yuanbao/new.ts +81 -0
- package/src/clis/yuanbao/shared.ts +57 -0
- package/src/clis/zhihu/question.test.ts +42 -17
- package/src/clis/zhihu/question.ts +31 -26
- package/src/commanderAdapter.test.ts +51 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/completion.test.ts +30 -0
- package/src/completion.ts +3 -1
- package/src/doctor.ts +1 -1
- package/src/electron-apps.ts +9 -1
- package/src/errors.ts +1 -1
- package/src/execution.ts +26 -30
- package/src/explore.ts +1 -1
- package/src/launcher.test.ts +121 -7
- package/src/launcher.ts +87 -9
- package/src/output.test.ts +50 -90
- package/src/output.ts +10 -1
- package/src/pipeline/executor.test.ts +0 -2
- package/src/pipeline/steps/download.test.ts +0 -2
- package/src/registry.ts +2 -0
- package/src/serialization.ts +2 -0
- package/src/types.ts +9 -2
- package/tests/e2e/browser-auth.test.ts +9 -0
- package/CLI-EXPLORER.md +0 -724
- package/CLI-ONESHOT.md +0 -216
- package/SKILL.md +0 -59
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AuthRequiredError } from '../../errors.js';
|
|
2
|
+
export const YUANBAO_DOMAIN = 'yuanbao.tencent.com';
|
|
3
|
+
export const YUANBAO_URL = 'https://yuanbao.tencent.com/';
|
|
4
|
+
const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing yuanbao.tencent.com browser session.';
|
|
5
|
+
/**
|
|
6
|
+
* Reusable visibility check for injected browser scripts.
|
|
7
|
+
* Embed in page.evaluate strings via `${IS_VISIBLE_JS}`.
|
|
8
|
+
*/
|
|
9
|
+
export const IS_VISIBLE_JS = `const isVisible = (node) => {
|
|
10
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
11
|
+
const rect = node.getBoundingClientRect();
|
|
12
|
+
const style = window.getComputedStyle(node);
|
|
13
|
+
return rect.width > 0
|
|
14
|
+
&& rect.height > 0
|
|
15
|
+
&& style.display !== 'none'
|
|
16
|
+
&& style.visibility !== 'hidden';
|
|
17
|
+
};`;
|
|
18
|
+
export function authRequired(message) {
|
|
19
|
+
return new AuthRequiredError(YUANBAO_DOMAIN, `${message} ${SESSION_HINT}`);
|
|
20
|
+
}
|
|
21
|
+
export async function isOnYuanbao(page) {
|
|
22
|
+
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
23
|
+
if (typeof url !== 'string' || !url)
|
|
24
|
+
return false;
|
|
25
|
+
try {
|
|
26
|
+
const hostname = new URL(url).hostname;
|
|
27
|
+
return hostname === YUANBAO_DOMAIN || hostname.endsWith(`.${YUANBAO_DOMAIN}`);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function ensureYuanbaoPage(page) {
|
|
34
|
+
if (!(await isOnYuanbao(page))) {
|
|
35
|
+
await page.goto(YUANBAO_URL, { waitUntil: 'load', settleMs: 2500 });
|
|
36
|
+
await page.wait(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function hasLoginGate(page) {
|
|
40
|
+
const result = await page.evaluate(`(() => {
|
|
41
|
+
const bodyText = document.body.innerText || '';
|
|
42
|
+
const hasWechatLoginText = bodyText.includes('微信扫码登录');
|
|
43
|
+
const hasWechatIframe = Array.from(document.querySelectorAll('iframe'))
|
|
44
|
+
.some((frame) => (frame.getAttribute('src') || '').includes('open.weixin.qq.com/connect/qrconnect'));
|
|
45
|
+
|
|
46
|
+
return hasWechatLoginText || hasWechatIframe;
|
|
47
|
+
})()`);
|
|
48
|
+
return Boolean(result);
|
|
49
|
+
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { AuthRequiredError, CliError } from '../../errors.js';
|
|
3
|
+
function stripHtml(html) {
|
|
4
|
+
return html
|
|
5
|
+
.replace(/<[^>]+>/g, '')
|
|
6
|
+
.replace(/ /g, ' ')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.trim();
|
|
11
|
+
}
|
|
3
12
|
cli({
|
|
4
13
|
site: 'zhihu',
|
|
5
14
|
name: 'question',
|
|
@@ -13,30 +22,32 @@ cli({
|
|
|
13
22
|
columns: ['rank', 'author', 'votes', 'content'],
|
|
14
23
|
func: async (page, kwargs) => {
|
|
15
24
|
const { id, limit = 5 } = kwargs;
|
|
25
|
+
const questionId = String(id);
|
|
26
|
+
if (!/^\d+$/.test(questionId)) {
|
|
27
|
+
throw new CliError('INVALID_INPUT', 'Question ID must be numeric', 'Example: opencli zhihu question 123456789');
|
|
28
|
+
}
|
|
16
29
|
const answerLimit = Number(limit);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (result?.status === 401 || result?.status === 403) {
|
|
30
|
+
await page.goto(`https://www.zhihu.com/question/${questionId}`);
|
|
31
|
+
const url = `https://www.zhihu.com/api/v4/questions/${questionId}/answers?limit=${answerLimit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author`;
|
|
32
|
+
const data = await page.evaluate(`
|
|
33
|
+
(async () => {
|
|
34
|
+
const r = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
|
|
35
|
+
if (!r.ok) return { __httpError: r.status };
|
|
36
|
+
return await r.json();
|
|
37
|
+
})()
|
|
38
|
+
`);
|
|
39
|
+
if (!data || data.__httpError) {
|
|
40
|
+
const status = data?.__httpError;
|
|
41
|
+
if (status === 401 || status === 403) {
|
|
30
42
|
throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
|
|
31
43
|
}
|
|
32
|
-
throw new CliError('FETCH_ERROR', `Zhihu question answers request failed
|
|
44
|
+
throw new CliError('FETCH_ERROR', status ? `Zhihu question answers request failed (HTTP ${status})` : 'Zhihu question answers request failed', 'Try again later or rerun with -v for more detail');
|
|
33
45
|
}
|
|
34
|
-
|
|
46
|
+
return (data.data || []).map((item, i) => ({
|
|
35
47
|
rank: i + 1,
|
|
36
|
-
author:
|
|
37
|
-
votes:
|
|
38
|
-
content: stripHtml(
|
|
48
|
+
author: item.author?.name || 'anonymous',
|
|
49
|
+
votes: item.voteup_count || 0,
|
|
50
|
+
content: stripHtml(item.content || '').substring(0, 200),
|
|
39
51
|
}));
|
|
40
|
-
return answers;
|
|
41
52
|
},
|
|
42
53
|
});
|
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getRegistry } from '../../registry.js';
|
|
3
|
-
import { AuthRequiredError } from '../../errors.js';
|
|
3
|
+
import { AuthRequiredError, CliError } from '../../errors.js';
|
|
4
4
|
import './question.js';
|
|
5
5
|
describe('zhihu question', () => {
|
|
6
|
-
it('returns answers
|
|
6
|
+
it('returns answers from the Zhihu API', async () => {
|
|
7
7
|
const cmd = getRegistry().get('zhihu/question');
|
|
8
8
|
expect(cmd?.func).toBeTypeOf('function');
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const goto = vi.fn().mockResolvedValue(undefined);
|
|
10
|
+
const evaluate = vi.fn().mockImplementation(async (js) => {
|
|
11
|
+
expect(js).toContain('questions/2021881398772981878/answers?limit=3');
|
|
12
|
+
expect(js).toContain("credentials: 'include'");
|
|
11
13
|
return {
|
|
12
|
-
|
|
13
|
-
answers: [
|
|
14
|
+
data: [
|
|
14
15
|
{
|
|
15
16
|
author: { name: 'alice' },
|
|
16
17
|
voteup_count: 12,
|
|
17
|
-
content: '
|
|
18
|
+
content: 'Hello Zhihu',
|
|
18
19
|
},
|
|
19
20
|
],
|
|
20
21
|
};
|
|
21
22
|
});
|
|
22
|
-
const page = {
|
|
23
|
-
evaluate,
|
|
24
|
-
};
|
|
23
|
+
const page = { goto, evaluate };
|
|
25
24
|
await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).resolves.toEqual([
|
|
26
25
|
{
|
|
27
26
|
rank: 1,
|
|
@@ -30,25 +29,44 @@ describe('zhihu question', () => {
|
|
|
30
29
|
content: 'Hello Zhihu',
|
|
31
30
|
},
|
|
32
31
|
]);
|
|
32
|
+
expect(goto).toHaveBeenCalledWith('https://www.zhihu.com/question/2021881398772981878');
|
|
33
33
|
expect(evaluate).toHaveBeenCalledTimes(1);
|
|
34
34
|
});
|
|
35
35
|
it('maps auth-like answer failures to AuthRequiredError', async () => {
|
|
36
36
|
const cmd = getRegistry().get('zhihu/question');
|
|
37
|
-
expect(cmd?.func).toBeTypeOf('function');
|
|
38
37
|
const page = {
|
|
39
|
-
|
|
38
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
39
|
+
evaluate: vi.fn().mockResolvedValue({ __httpError: 403 }),
|
|
40
40
|
};
|
|
41
41
|
await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
42
42
|
});
|
|
43
|
-
it('preserves non-auth fetch failures as CliError
|
|
43
|
+
it('preserves non-auth fetch failures as CliError', async () => {
|
|
44
44
|
const cmd = getRegistry().get('zhihu/question');
|
|
45
|
-
expect(cmd?.func).toBeTypeOf('function');
|
|
46
45
|
const page = {
|
|
47
|
-
|
|
46
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
47
|
+
evaluate: vi.fn().mockResolvedValue({ __httpError: 500 }),
|
|
48
48
|
};
|
|
49
49
|
await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toMatchObject({
|
|
50
50
|
code: 'FETCH_ERROR',
|
|
51
|
-
message: 'Zhihu question answers request failed
|
|
51
|
+
message: 'Zhihu question answers request failed (HTTP 500)',
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
|
+
it('handles null evaluate response as fetch error', async () => {
|
|
55
|
+
const cmd = getRegistry().get('zhihu/question');
|
|
56
|
+
const page = {
|
|
57
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
evaluate: vi.fn().mockResolvedValue(null),
|
|
59
|
+
};
|
|
60
|
+
await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toMatchObject({
|
|
61
|
+
code: 'FETCH_ERROR',
|
|
62
|
+
message: 'Zhihu question answers request failed',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it('rejects non-numeric question IDs', async () => {
|
|
66
|
+
const cmd = getRegistry().get('zhihu/question');
|
|
67
|
+
const page = { goto: vi.fn(), evaluate: vi.fn() };
|
|
68
|
+
await expect(cmd.func(page, { id: "abc'; alert(1); //", limit: 1 })).rejects.toBeInstanceOf(CliError);
|
|
69
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
70
|
+
expect(page.evaluate).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
54
72
|
});
|
package/dist/commanderAdapter.js
CHANGED
|
@@ -49,7 +49,8 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
49
49
|
positionalArgs.push(arg);
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
|
-
const
|
|
52
|
+
const expectsValue = arg.required || arg.valueRequired;
|
|
53
|
+
const flag = expectsValue ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
53
54
|
if (arg.required)
|
|
54
55
|
subCmd.requiredOption(flag, arg.help ?? '');
|
|
55
56
|
else if (arg.default != null)
|
|
@@ -83,8 +84,10 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
83
84
|
if (v !== undefined)
|
|
84
85
|
kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
|
|
85
86
|
}
|
|
87
|
+
cmd.validateArgs?.(kwargs);
|
|
86
88
|
const verbose = optionsRecord.verbose === true;
|
|
87
89
|
let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
90
|
+
const formatExplicit = subCmd.getOptionValueSource('format') === 'cli';
|
|
88
91
|
if (verbose)
|
|
89
92
|
process.env.OPENCLI_VERBOSE = '1';
|
|
90
93
|
if (cmd.deprecated) {
|
|
@@ -97,7 +100,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
97
100
|
return;
|
|
98
101
|
}
|
|
99
102
|
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
100
|
-
if (format === 'table' && resolved.defaultFormat) {
|
|
103
|
+
if (!formatExplicit && format === 'table' && resolved.defaultFormat) {
|
|
101
104
|
format = resolved.defaultFormat;
|
|
102
105
|
}
|
|
103
106
|
if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
@@ -105,6 +108,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
105
108
|
}
|
|
106
109
|
renderOutput(result, {
|
|
107
110
|
fmt: format,
|
|
111
|
+
fmtExplicit: formatExplicit,
|
|
108
112
|
columns: resolved.columns,
|
|
109
113
|
title: `${resolved.site}/${resolved.name}`,
|
|
110
114
|
elapsed: (Date.now() - startTime) / 1000,
|
|
@@ -198,7 +202,7 @@ async function renderError(err, cmdName, verbose) {
|
|
|
198
202
|
if (err instanceof AuthRequiredError) {
|
|
199
203
|
console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
|
|
200
204
|
// Respect custom hints set by the adapter; fall back to generic guidance.
|
|
201
|
-
console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
|
|
205
|
+
console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome or Chromium and log in to https://${err.domain}, then retry.`}`));
|
|
202
206
|
return;
|
|
203
207
|
}
|
|
204
208
|
// ── TimeoutError ──────────────────────────────────────────────────────
|
|
@@ -255,7 +259,7 @@ async function renderError(err, cmdName, verbose) {
|
|
|
255
259
|
const kind = classifyGenericError(msg);
|
|
256
260
|
if (kind === 'auth') {
|
|
257
261
|
console.error(chalk.red(`🔒 ${msg}`));
|
|
258
|
-
console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
|
|
262
|
+
console.error(chalk.yellow('→ Open Chrome or Chromium, log in to the target site, then retry.'));
|
|
259
263
|
return;
|
|
260
264
|
}
|
|
261
265
|
if (kind === 'http') {
|
|
@@ -100,6 +100,48 @@ describe('commanderAdapter boolean alias support', () => {
|
|
|
100
100
|
expect(kwargs.undo).toBe(false);
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
|
+
describe('commanderAdapter value-required optional options', () => {
|
|
104
|
+
const cmd = {
|
|
105
|
+
site: 'instagram',
|
|
106
|
+
name: 'post',
|
|
107
|
+
description: 'Post to Instagram',
|
|
108
|
+
browser: true,
|
|
109
|
+
args: [
|
|
110
|
+
{ name: 'image', valueRequired: true, help: 'Single image path' },
|
|
111
|
+
{ name: 'images', valueRequired: true, help: 'Comma-separated image paths' },
|
|
112
|
+
{ name: 'content', positional: true, required: false, help: 'Caption text' },
|
|
113
|
+
],
|
|
114
|
+
validateArgs: (kwargs) => {
|
|
115
|
+
if (!kwargs.image && !kwargs.images) {
|
|
116
|
+
throw new Error('media required');
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
func: vi.fn(),
|
|
120
|
+
};
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
mockExecuteCommand.mockReset();
|
|
123
|
+
mockExecuteCommand.mockResolvedValue([]);
|
|
124
|
+
mockRenderOutput.mockReset();
|
|
125
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
126
|
+
process.exitCode = undefined;
|
|
127
|
+
});
|
|
128
|
+
it('requires a value when --image is present', async () => {
|
|
129
|
+
const program = new Command();
|
|
130
|
+
program.exitOverride();
|
|
131
|
+
const siteCmd = program.command('instagram');
|
|
132
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
133
|
+
await expect(program.parseAsync(['node', 'opencli', 'instagram', 'post', '--image'])).rejects.toMatchObject({ code: 'commander.optionMissingArgument' });
|
|
134
|
+
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
it('runs validateArgs before executeCommand so missing media does not dispatch the browser command', async () => {
|
|
137
|
+
const program = new Command();
|
|
138
|
+
const siteCmd = program.command('instagram');
|
|
139
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
140
|
+
await program.parseAsync(['node', 'opencli', 'instagram', 'post', 'caption only']);
|
|
141
|
+
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
142
|
+
expect(process.exitCode).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
103
145
|
describe('commanderAdapter command aliases', () => {
|
|
104
146
|
const cmd = {
|
|
105
147
|
site: 'notebooklm',
|
package/dist/completion.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockGetRegistry } = vi.hoisted(() => ({
|
|
3
|
+
mockGetRegistry: vi.fn(() => new Map([
|
|
4
|
+
['github/issues', { site: 'github', name: 'issues' }],
|
|
5
|
+
])),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('./registry.js', () => ({
|
|
8
|
+
getRegistry: mockGetRegistry,
|
|
9
|
+
}));
|
|
10
|
+
import { getCompletions } from './completion.js';
|
|
11
|
+
describe('getCompletions', () => {
|
|
12
|
+
it('includes top-level built-ins that are registered outside the site registry', () => {
|
|
13
|
+
const completions = getCompletions([], 1);
|
|
14
|
+
expect(completions).toContain('plugin');
|
|
15
|
+
expect(completions).toContain('install');
|
|
16
|
+
expect(completions).toContain('register');
|
|
17
|
+
expect(completions).not.toContain('setup');
|
|
18
|
+
});
|
|
19
|
+
it('still includes discovered site names', () => {
|
|
20
|
+
const completions = getCompletions([], 1);
|
|
21
|
+
expect(completions).toContain('github');
|
|
22
|
+
});
|
|
23
|
+
});
|
package/dist/doctor.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
55
55
|
issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
|
|
56
56
|
}
|
|
57
57
|
if (status.running && !status.extensionConnected) {
|
|
58
|
-
issues.push('Daemon is running but the Chrome extension is not connected.\n' +
|
|
58
|
+
issues.push('Daemon is running but the Chrome/Chromium extension is not connected.\n' +
|
|
59
59
|
'Please install the opencli Browser Bridge extension:\n' +
|
|
60
60
|
' 1. Download from https://github.com/jackwener/opencli/releases\n' +
|
|
61
61
|
' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
|
package/dist/electron-apps.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface ElectronAppEntry {
|
|
|
9
9
|
port: number;
|
|
10
10
|
/** macOS process name for detection via pgrep */
|
|
11
11
|
processName: string;
|
|
12
|
+
/** Candidate executable names inside Contents/MacOS/, tried in order */
|
|
13
|
+
executableNames?: string[];
|
|
12
14
|
/** macOS bundle ID for path discovery */
|
|
13
15
|
bundleId?: string;
|
|
14
16
|
/** Human-readable name for prompts */
|
package/dist/electron-apps.js
CHANGED
|
@@ -15,7 +15,13 @@ export const builtinApps = {
|
|
|
15
15
|
notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' },
|
|
16
16
|
'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' },
|
|
17
17
|
'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' },
|
|
18
|
-
antigravity: {
|
|
18
|
+
antigravity: {
|
|
19
|
+
port: 9234,
|
|
20
|
+
processName: 'Antigravity',
|
|
21
|
+
executableNames: ['Electron', 'Antigravity'],
|
|
22
|
+
bundleId: 'dev.antigravity.app',
|
|
23
|
+
displayName: 'Antigravity',
|
|
24
|
+
},
|
|
19
25
|
chatgpt: { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' },
|
|
20
26
|
};
|
|
21
27
|
/** Merge builtin + user-defined apps. User entries are additive only. */
|
package/dist/errors.js
CHANGED
|
@@ -72,7 +72,7 @@ export class ConfigError extends CliError {
|
|
|
72
72
|
export class AuthRequiredError extends CliError {
|
|
73
73
|
domain;
|
|
74
74
|
constructor(domain, message) {
|
|
75
|
-
super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`, EXIT_CODES.NOPERM);
|
|
75
|
+
super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome or Chromium and log in to https://${domain}`, EXIT_CODES.NOPERM);
|
|
76
76
|
this.domain = domain;
|
|
77
77
|
}
|
|
78
78
|
}
|
package/dist/execution.js
CHANGED
|
@@ -19,7 +19,7 @@ import { emitHook } from './hooks.js';
|
|
|
19
19
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
20
20
|
import { log } from './logger.js';
|
|
21
21
|
import { isElectronApp } from './electron-apps.js';
|
|
22
|
-
import { resolveElectronEndpoint } from './launcher.js';
|
|
22
|
+
import { probeCDP, resolveElectronEndpoint } from './launcher.js';
|
|
23
23
|
const _loadedModules = new Set();
|
|
24
24
|
export function coerceAndValidateArgs(cmdArgs, kwargs) {
|
|
25
25
|
const result = { ...kwargs };
|
|
@@ -111,29 +111,11 @@ function ensureRequiredEnv(cmd) {
|
|
|
111
111
|
return;
|
|
112
112
|
throw new CommandExecutionError(`Command ${fullName(cmd)} requires environment variable ${missing.name}.`, missing.help ?? `Set ${missing.name} before running ${fullName(cmd)}.`);
|
|
113
113
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Check if the browser is already on the target domain, avoiding redundant navigation.
|
|
116
|
-
* Returns true if current page hostname matches the pre-nav URL hostname.
|
|
117
|
-
*/
|
|
118
|
-
async function isAlreadyOnDomain(page, targetUrl) {
|
|
119
|
-
if (!page.getCurrentUrl)
|
|
120
|
-
return false;
|
|
121
|
-
try {
|
|
122
|
-
const currentUrl = await page.getCurrentUrl();
|
|
123
|
-
if (!currentUrl)
|
|
124
|
-
return false;
|
|
125
|
-
const currentHost = new URL(currentUrl).hostname;
|
|
126
|
-
const targetHost = new URL(targetUrl).hostname;
|
|
127
|
-
return currentHost === targetHost;
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
114
|
export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
134
115
|
let kwargs;
|
|
135
116
|
try {
|
|
136
117
|
kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
|
|
118
|
+
cmd.validateArgs?.(kwargs);
|
|
137
119
|
}
|
|
138
120
|
catch (err) {
|
|
139
121
|
if (err instanceof ArgumentError)
|
|
@@ -152,8 +134,18 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
|
152
134
|
const electron = isElectronApp(cmd.site);
|
|
153
135
|
let cdpEndpoint;
|
|
154
136
|
if (electron) {
|
|
155
|
-
// Electron apps:
|
|
156
|
-
|
|
137
|
+
// Electron apps: respect manual endpoint override, then try auto-detect
|
|
138
|
+
const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
139
|
+
if (manualEndpoint) {
|
|
140
|
+
const port = Number(new URL(manualEndpoint).port);
|
|
141
|
+
if (!await probeCDP(port)) {
|
|
142
|
+
throw new CommandExecutionError(`CDP not reachable at ${manualEndpoint}`, 'Check that the app is running with --remote-debugging-port and the endpoint is correct.');
|
|
143
|
+
}
|
|
144
|
+
cdpEndpoint = manualEndpoint;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
|
|
148
|
+
}
|
|
157
149
|
}
|
|
158
150
|
else {
|
|
159
151
|
// Browser Bridge: fail-fast when daemon is up but extension is missing.
|
|
@@ -162,7 +154,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
|
162
154
|
if (status.running && !status.extensionConnected) {
|
|
163
155
|
throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
|
|
164
156
|
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
165
|
-
' 2. chrome://extensions → Developer Mode → Load unpacked\n' +
|
|
157
|
+
' 2. In Chrome or Chromium, open chrome://extensions → Developer Mode → Load unpacked\n' +
|
|
166
158
|
' Then run: opencli doctor');
|
|
167
159
|
}
|
|
168
160
|
}
|
|
@@ -171,19 +163,17 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
|
171
163
|
result = await browserSession(BrowserFactory, async (page) => {
|
|
172
164
|
const preNavUrl = resolvePreNav(cmd);
|
|
173
165
|
if (preNavUrl) {
|
|
174
|
-
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
|
|
166
|
+
// Navigate directly — the extension's handleNavigate already has a fast-path
|
|
167
|
+
// that skips navigation if the tab is already at the target URL.
|
|
168
|
+
// This avoids an extra exec round-trip (getCurrentUrl) on first command and
|
|
169
|
+
// lets the extension create the automation window with the target URL directly
|
|
170
|
+
// instead of about:blank.
|
|
171
|
+
try {
|
|
172
|
+
await page.goto(preNavUrl);
|
|
178
173
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
if (debug)
|
|
185
|
-
log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
|
|
186
|
-
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (debug)
|
|
176
|
+
log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
|
|
187
177
|
}
|
|
188
178
|
}
|
|
189
179
|
return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
|
package/dist/explore.js
CHANGED
|
@@ -263,7 +263,7 @@ export async function exploreUrl(url, opts) {
|
|
|
263
263
|
await page.wait(2); // wait for XHRs to settle
|
|
264
264
|
}
|
|
265
265
|
catch (e) {
|
|
266
|
-
log.
|
|
266
|
+
log.verbose(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
// Step 3: Read page metadata
|
package/dist/launcher.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 4. Launch with --remote-debugging-port
|
|
9
9
|
* 5. Poll /json until ready
|
|
10
10
|
*/
|
|
11
|
+
import type { ElectronAppEntry } from './electron-apps.js';
|
|
11
12
|
/**
|
|
12
13
|
* Probe whether a CDP endpoint is listening on the given port.
|
|
13
14
|
* Returns true if http://127.0.0.1:{port}/json responds successfully.
|
|
@@ -28,6 +29,9 @@ export declare function killProcess(processName: string): void;
|
|
|
28
29
|
* Returns null if the app is not installed.
|
|
29
30
|
*/
|
|
30
31
|
export declare function discoverAppPath(displayName: string): string | null;
|
|
32
|
+
export declare function resolveExecutableCandidates(appPath: string, app: ElectronAppEntry): string[];
|
|
33
|
+
export declare function launchDetachedApp(executable: string, args: string[], label: string): Promise<void>;
|
|
34
|
+
export declare function launchElectronApp(appPath: string, app: ElectronAppEntry, args: string[], label: string): Promise<void>;
|
|
31
35
|
/**
|
|
32
36
|
* Main entry point: resolve an Electron app to a CDP endpoint URL.
|
|
33
37
|
*
|
package/dist/launcher.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync, spawn } from 'node:child_process';
|
|
12
12
|
import { request as httpRequest } from 'node:http';
|
|
13
|
+
import * as path from 'node:path';
|
|
13
14
|
import { getElectronApp } from './electron-apps.js';
|
|
14
15
|
import { confirmPrompt } from './tui.js';
|
|
15
16
|
import { CommandExecutionError } from './errors.js';
|
|
@@ -38,6 +39,8 @@ export function probeCDP(port, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
|
38
39
|
* Uses pgrep on macOS/Linux.
|
|
39
40
|
*/
|
|
40
41
|
export function detectProcess(processName) {
|
|
42
|
+
if (process.platform === 'win32')
|
|
43
|
+
return false; // pgrep not available on Windows
|
|
41
44
|
try {
|
|
42
45
|
execFileSync('pgrep', ['-x', processName], { encoding: 'utf-8', stdio: 'pipe' });
|
|
43
46
|
return true;
|
|
@@ -50,6 +53,8 @@ export function detectProcess(processName) {
|
|
|
50
53
|
* Kill a process by name. Sends SIGTERM first, then SIGKILL after grace period.
|
|
51
54
|
*/
|
|
52
55
|
export function killProcess(processName) {
|
|
56
|
+
if (process.platform === 'win32')
|
|
57
|
+
return; // pkill not available on Windows
|
|
53
58
|
try {
|
|
54
59
|
execFileSync('pkill', ['-x', processName], { stdio: 'pipe' });
|
|
55
60
|
}
|
|
@@ -91,6 +96,57 @@ export function discoverAppPath(displayName) {
|
|
|
91
96
|
function resolveExecutable(appPath, processName) {
|
|
92
97
|
return `${appPath}/Contents/MacOS/${processName}`;
|
|
93
98
|
}
|
|
99
|
+
function isMissingExecutableError(err, label) {
|
|
100
|
+
return err instanceof CommandExecutionError
|
|
101
|
+
&& err.message.startsWith(`Could not launch ${label}: executable not found at `);
|
|
102
|
+
}
|
|
103
|
+
export function resolveExecutableCandidates(appPath, app) {
|
|
104
|
+
const executableNames = app.executableNames?.length ? app.executableNames : [app.processName];
|
|
105
|
+
return [...new Set(executableNames)].map((name) => resolveExecutable(appPath, name));
|
|
106
|
+
}
|
|
107
|
+
export async function launchDetachedApp(executable, args, label) {
|
|
108
|
+
await new Promise((resolve, reject) => {
|
|
109
|
+
const child = spawn(executable, args, {
|
|
110
|
+
detached: true,
|
|
111
|
+
stdio: 'ignore',
|
|
112
|
+
});
|
|
113
|
+
const onError = (err) => {
|
|
114
|
+
if (err.code === 'ENOENT') {
|
|
115
|
+
reject(new CommandExecutionError(`Could not launch ${label}: executable not found at ${executable}`, `Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
reject(new CommandExecutionError(`Failed to launch ${label}`, err.message));
|
|
119
|
+
};
|
|
120
|
+
child.once('error', onError);
|
|
121
|
+
child.once('spawn', () => {
|
|
122
|
+
child.off('error', onError);
|
|
123
|
+
child.unref();
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export async function launchElectronApp(appPath, app, args, label) {
|
|
129
|
+
const executables = resolveExecutableCandidates(appPath, app);
|
|
130
|
+
let lastMissingExecutableError;
|
|
131
|
+
for (const executable of executables) {
|
|
132
|
+
log.debug(`[launcher] Launching: ${executable} ${args.join(' ')}`);
|
|
133
|
+
try {
|
|
134
|
+
await launchDetachedApp(executable, args, label);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (isMissingExecutableError(err, label)) {
|
|
139
|
+
lastMissingExecutableError = err;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (executables.length > 1) {
|
|
146
|
+
throw new CommandExecutionError(`Could not launch ${label}: no compatible executable found in ${path.join(appPath, 'Contents', 'MacOS')}`, `Tried: ${executables.map((executable) => path.basename(executable)).join(', ')}. Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`);
|
|
147
|
+
}
|
|
148
|
+
throw lastMissingExecutableError ?? new CommandExecutionError(`Could not launch ${label}`, `Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`);
|
|
149
|
+
}
|
|
94
150
|
async function pollForReady(port) {
|
|
95
151
|
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
96
152
|
while (Date.now() < deadline) {
|
|
@@ -119,7 +175,13 @@ export async function resolveElectronEndpoint(site) {
|
|
|
119
175
|
log.debug(`[launcher] CDP already available on port ${port}`);
|
|
120
176
|
return endpoint;
|
|
121
177
|
}
|
|
122
|
-
// Step 2: Running without CDP?
|
|
178
|
+
// Step 2: Running without CDP? (process detection requires Unix tools)
|
|
179
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
180
|
+
throw new CommandExecutionError(`${label} is not reachable on CDP port ${port}.`, `Auto-launch is not yet supported on ${process.platform}.\n` +
|
|
181
|
+
`Start ${label} manually with --remote-debugging-port=${port}, then either:\n` +
|
|
182
|
+
` • Set OPENCLI_CDP_ENDPOINT=http://127.0.0.1:${port}\n` +
|
|
183
|
+
` • Or just re-run the command once ${label} is listening on port ${port}.`);
|
|
184
|
+
}
|
|
123
185
|
const isRunning = detectProcess(processName);
|
|
124
186
|
if (isRunning) {
|
|
125
187
|
log.debug(`[launcher] ${label} is running but CDP not available`);
|
|
@@ -136,14 +198,8 @@ export async function resolveElectronEndpoint(site) {
|
|
|
136
198
|
throw new CommandExecutionError(`Could not find ${label} on this machine.`, `Install ${label} or register a custom path in ~/.opencli/apps.yaml`);
|
|
137
199
|
}
|
|
138
200
|
// Step 4: Launch
|
|
139
|
-
const executable = resolveExecutable(appPath, processName);
|
|
140
201
|
const args = [`--remote-debugging-port=${port}`, ...(app.extraArgs ?? [])];
|
|
141
|
-
|
|
142
|
-
const child = spawn(executable, args, {
|
|
143
|
-
detached: true,
|
|
144
|
-
stdio: 'ignore',
|
|
145
|
-
});
|
|
146
|
-
child.unref();
|
|
202
|
+
await launchElectronApp(appPath, app, args, label);
|
|
147
203
|
// Step 5: Poll for readiness
|
|
148
204
|
process.stderr.write(` Waiting for ${label} on port ${port}...\n`);
|
|
149
205
|
await pollForReady(port);
|