@jackwener/opencli 1.6.1 → 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/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/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 +1135 -184
- 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/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/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
|
@@ -1,30 +1,29 @@
|
|
|
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
|
|
|
6
6
|
describe('zhihu question', () => {
|
|
7
|
-
it('returns answers
|
|
7
|
+
it('returns answers from the Zhihu API', async () => {
|
|
8
8
|
const cmd = getRegistry().get('zhihu/question');
|
|
9
9
|
expect(cmd?.func).toBeTypeOf('function');
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const goto = vi.fn().mockResolvedValue(undefined);
|
|
12
|
+
const evaluate = vi.fn().mockImplementation(async (js: string) => {
|
|
13
|
+
expect(js).toContain('questions/2021881398772981878/answers?limit=3');
|
|
14
|
+
expect(js).toContain("credentials: 'include'");
|
|
13
15
|
return {
|
|
14
|
-
|
|
15
|
-
answers: [
|
|
16
|
+
data: [
|
|
16
17
|
{
|
|
17
18
|
author: { name: 'alice' },
|
|
18
19
|
voteup_count: 12,
|
|
19
|
-
content: '
|
|
20
|
+
content: 'Hello Zhihu',
|
|
20
21
|
},
|
|
21
22
|
],
|
|
22
23
|
};
|
|
23
24
|
});
|
|
24
25
|
|
|
25
|
-
const page = {
|
|
26
|
-
evaluate,
|
|
27
|
-
} as any;
|
|
26
|
+
const page = { goto, evaluate } as any;
|
|
28
27
|
|
|
29
28
|
await expect(
|
|
30
29
|
cmd!.func!(page, { id: '2021881398772981878', limit: 3 }),
|
|
@@ -37,15 +36,15 @@ describe('zhihu question', () => {
|
|
|
37
36
|
},
|
|
38
37
|
]);
|
|
39
38
|
|
|
39
|
+
expect(goto).toHaveBeenCalledWith('https://www.zhihu.com/question/2021881398772981878');
|
|
40
40
|
expect(evaluate).toHaveBeenCalledTimes(1);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it('maps auth-like answer failures to AuthRequiredError', async () => {
|
|
44
44
|
const cmd = getRegistry().get('zhihu/question');
|
|
45
|
-
expect(cmd?.func).toBeTypeOf('function');
|
|
46
|
-
|
|
47
45
|
const page = {
|
|
48
|
-
|
|
46
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
47
|
+
evaluate: vi.fn().mockResolvedValue({ __httpError: 403 }),
|
|
49
48
|
} as any;
|
|
50
49
|
|
|
51
50
|
await expect(
|
|
@@ -53,19 +52,45 @@ describe('zhihu question', () => {
|
|
|
53
52
|
).rejects.toBeInstanceOf(AuthRequiredError);
|
|
54
53
|
});
|
|
55
54
|
|
|
56
|
-
it('preserves non-auth fetch failures as CliError
|
|
55
|
+
it('preserves non-auth fetch failures as CliError', async () => {
|
|
57
56
|
const cmd = getRegistry().get('zhihu/question');
|
|
58
|
-
|
|
57
|
+
const page = {
|
|
58
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
evaluate: vi.fn().mockResolvedValue({ __httpError: 500 }),
|
|
60
|
+
} as any;
|
|
59
61
|
|
|
62
|
+
await expect(
|
|
63
|
+
cmd!.func!(page, { id: '2021881398772981878', limit: 3 }),
|
|
64
|
+
).rejects.toMatchObject({
|
|
65
|
+
code: 'FETCH_ERROR',
|
|
66
|
+
message: 'Zhihu question answers request failed (HTTP 500)',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('handles null evaluate response as fetch error', async () => {
|
|
71
|
+
const cmd = getRegistry().get('zhihu/question');
|
|
60
72
|
const page = {
|
|
61
|
-
|
|
73
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
74
|
+
evaluate: vi.fn().mockResolvedValue(null),
|
|
62
75
|
} as any;
|
|
63
76
|
|
|
64
77
|
await expect(
|
|
65
78
|
cmd!.func!(page, { id: '2021881398772981878', limit: 3 }),
|
|
66
79
|
).rejects.toMatchObject({
|
|
67
80
|
code: 'FETCH_ERROR',
|
|
68
|
-
message: 'Zhihu question answers request failed
|
|
81
|
+
message: 'Zhihu question answers request failed',
|
|
69
82
|
});
|
|
70
83
|
});
|
|
84
|
+
|
|
85
|
+
it('rejects non-numeric question IDs', async () => {
|
|
86
|
+
const cmd = getRegistry().get('zhihu/question');
|
|
87
|
+
const page = { goto: vi.fn(), evaluate: vi.fn() } as any;
|
|
88
|
+
|
|
89
|
+
await expect(
|
|
90
|
+
cmd!.func!(page, { id: "abc'; alert(1); //", limit: 1 }),
|
|
91
|
+
).rejects.toBeInstanceOf(CliError);
|
|
92
|
+
|
|
93
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
94
|
+
expect(page.evaluate).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
71
96
|
});
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { AuthRequiredError, CliError } from '../../errors.js';
|
|
3
3
|
|
|
4
|
+
function stripHtml(html: string): string {
|
|
5
|
+
return html
|
|
6
|
+
.replace(/<[^>]+>/g, '')
|
|
7
|
+
.replace(/ /g, ' ')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
cli({
|
|
5
15
|
site: 'zhihu',
|
|
6
16
|
name: 'question',
|
|
@@ -14,45 +24,40 @@ cli({
|
|
|
14
24
|
columns: ['rank', 'author', 'votes', 'content'],
|
|
15
25
|
func: async (page, kwargs) => {
|
|
16
26
|
const { id, limit = 5 } = kwargs;
|
|
27
|
+
const questionId = String(id);
|
|
28
|
+
if (!/^\d+$/.test(questionId)) {
|
|
29
|
+
throw new CliError('INVALID_INPUT', 'Question ID must be numeric', 'Example: opencli zhihu question 123456789');
|
|
30
|
+
}
|
|
17
31
|
const answerLimit = Number(limit);
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
(html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
33
|
+
await page.goto(`https://www.zhihu.com/question/${questionId}`);
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
31
|
-
if (!aResp.ok) return { ok: false as const, status: aResp.status };
|
|
32
|
-
const a = await aResp.json();
|
|
33
|
-
return { ok: true as const, answers: Array.isArray(a?.data) ? a.data : [] };
|
|
34
|
-
},
|
|
35
|
-
{ questionId: String(id), answerLimit },
|
|
36
|
-
);
|
|
35
|
+
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`;
|
|
36
|
+
const data: any = await page.evaluate(`
|
|
37
|
+
(async () => {
|
|
38
|
+
const r = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
|
|
39
|
+
if (!r.ok) return { __httpError: r.status };
|
|
40
|
+
return await r.json();
|
|
41
|
+
})()
|
|
42
|
+
`);
|
|
37
43
|
|
|
38
|
-
if (!
|
|
39
|
-
|
|
44
|
+
if (!data || data.__httpError) {
|
|
45
|
+
const status = data?.__httpError;
|
|
46
|
+
if (status === 401 || status === 403) {
|
|
40
47
|
throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
|
|
41
48
|
}
|
|
42
49
|
throw new CliError(
|
|
43
50
|
'FETCH_ERROR',
|
|
44
|
-
`Zhihu question answers request failed
|
|
51
|
+
status ? `Zhihu question answers request failed (HTTP ${status})` : 'Zhihu question answers request failed',
|
|
45
52
|
'Try again later or rerun with -v for more detail',
|
|
46
53
|
);
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
return (data.data || []).map((item: any, i: number) => ({
|
|
50
57
|
rank: i + 1,
|
|
51
|
-
author:
|
|
52
|
-
votes:
|
|
53
|
-
content: stripHtml(
|
|
58
|
+
author: item.author?.name || 'anonymous',
|
|
59
|
+
votes: item.voteup_count || 0,
|
|
60
|
+
content: stripHtml(item.content || '').substring(0, 200),
|
|
54
61
|
}));
|
|
55
|
-
|
|
56
|
-
return answers;
|
|
57
62
|
},
|
|
58
63
|
});
|
|
@@ -125,6 +125,57 @@ describe('commanderAdapter boolean alias support', () => {
|
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
describe('commanderAdapter value-required optional options', () => {
|
|
129
|
+
const cmd: CliCommand = {
|
|
130
|
+
site: 'instagram',
|
|
131
|
+
name: 'post',
|
|
132
|
+
description: 'Post to Instagram',
|
|
133
|
+
browser: true,
|
|
134
|
+
args: [
|
|
135
|
+
{ name: 'image', valueRequired: true, help: 'Single image path' },
|
|
136
|
+
{ name: 'images', valueRequired: true, help: 'Comma-separated image paths' },
|
|
137
|
+
{ name: 'content', positional: true, required: false, help: 'Caption text' },
|
|
138
|
+
],
|
|
139
|
+
validateArgs: (kwargs) => {
|
|
140
|
+
if (!kwargs.image && !kwargs.images) {
|
|
141
|
+
throw new Error('media required');
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
func: vi.fn(),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
mockExecuteCommand.mockReset();
|
|
149
|
+
mockExecuteCommand.mockResolvedValue([]);
|
|
150
|
+
mockRenderOutput.mockReset();
|
|
151
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
152
|
+
process.exitCode = undefined;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('requires a value when --image is present', async () => {
|
|
156
|
+
const program = new Command();
|
|
157
|
+
program.exitOverride();
|
|
158
|
+
const siteCmd = program.command('instagram');
|
|
159
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
160
|
+
|
|
161
|
+
await expect(
|
|
162
|
+
program.parseAsync(['node', 'opencli', 'instagram', 'post', '--image']),
|
|
163
|
+
).rejects.toMatchObject({ code: 'commander.optionMissingArgument' });
|
|
164
|
+
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('runs validateArgs before executeCommand so missing media does not dispatch the browser command', async () => {
|
|
168
|
+
const program = new Command();
|
|
169
|
+
const siteCmd = program.command('instagram');
|
|
170
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
171
|
+
|
|
172
|
+
await program.parseAsync(['node', 'opencli', 'instagram', 'post', 'caption only']);
|
|
173
|
+
|
|
174
|
+
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
175
|
+
expect(process.exitCode).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
128
179
|
describe('commanderAdapter command aliases', () => {
|
|
129
180
|
const cmd: CliCommand = {
|
|
130
181
|
site: 'notebooklm',
|
package/src/commanderAdapter.ts
CHANGED
|
@@ -62,7 +62,8 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
62
62
|
subCmd.argument(bracket, arg.help ?? '');
|
|
63
63
|
positionalArgs.push(arg);
|
|
64
64
|
} else {
|
|
65
|
-
const
|
|
65
|
+
const expectsValue = arg.required || arg.valueRequired;
|
|
66
|
+
const flag = expectsValue ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
66
67
|
if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
|
|
67
68
|
else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
68
69
|
else subCmd.option(flag, arg.help ?? '');
|
|
@@ -93,9 +94,11 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
93
94
|
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
94
95
|
if (v !== undefined) kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
|
|
95
96
|
}
|
|
97
|
+
cmd.validateArgs?.(kwargs);
|
|
96
98
|
|
|
97
99
|
const verbose = optionsRecord.verbose === true;
|
|
98
100
|
let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
101
|
+
const formatExplicit = subCmd.getOptionValueSource('format') === 'cli';
|
|
99
102
|
if (verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
100
103
|
if (cmd.deprecated) {
|
|
101
104
|
const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
|
|
@@ -109,7 +112,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
112
|
-
if (format === 'table' && resolved.defaultFormat) {
|
|
115
|
+
if (!formatExplicit && format === 'table' && resolved.defaultFormat) {
|
|
113
116
|
format = resolved.defaultFormat;
|
|
114
117
|
}
|
|
115
118
|
|
|
@@ -118,6 +121,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
118
121
|
}
|
|
119
122
|
renderOutput(result, {
|
|
120
123
|
fmt: format,
|
|
124
|
+
fmtExplicit: formatExplicit,
|
|
121
125
|
columns: resolved.columns,
|
|
122
126
|
title: `${resolved.site}/${resolved.name}`,
|
|
123
127
|
elapsed: (Date.now() - startTime) / 1000,
|
|
@@ -209,7 +213,7 @@ async function renderError(err: unknown, cmdName: string, verbose: boolean): Pro
|
|
|
209
213
|
if (err instanceof AuthRequiredError) {
|
|
210
214
|
console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
|
|
211
215
|
// Respect custom hints set by the adapter; fall back to generic guidance.
|
|
212
|
-
console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
|
|
216
|
+
console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome or Chromium and log in to https://${err.domain}, then retry.`}`));
|
|
213
217
|
return;
|
|
214
218
|
}
|
|
215
219
|
|
|
@@ -270,7 +274,7 @@ async function renderError(err: unknown, cmdName: string, verbose: boolean): Pro
|
|
|
270
274
|
|
|
271
275
|
if (kind === 'auth') {
|
|
272
276
|
console.error(chalk.red(`🔒 ${msg}`));
|
|
273
|
-
console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
|
|
277
|
+
console.error(chalk.yellow('→ Open Chrome or Chromium, log in to the target site, then retry.'));
|
|
274
278
|
return;
|
|
275
279
|
}
|
|
276
280
|
if (kind === 'http') {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { mockGetRegistry } = vi.hoisted(() => ({
|
|
4
|
+
mockGetRegistry: vi.fn(() => new Map([
|
|
5
|
+
['github/issues', { site: 'github', name: 'issues' }],
|
|
6
|
+
])),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock('./registry.js', () => ({
|
|
10
|
+
getRegistry: mockGetRegistry,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { getCompletions } from './completion.js';
|
|
14
|
+
|
|
15
|
+
describe('getCompletions', () => {
|
|
16
|
+
it('includes top-level built-ins that are registered outside the site registry', () => {
|
|
17
|
+
const completions = getCompletions([], 1);
|
|
18
|
+
|
|
19
|
+
expect(completions).toContain('plugin');
|
|
20
|
+
expect(completions).toContain('install');
|
|
21
|
+
expect(completions).toContain('register');
|
|
22
|
+
expect(completions).not.toContain('setup');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('still includes discovered site names', () => {
|
|
26
|
+
const completions = getCompletions([], 1);
|
|
27
|
+
|
|
28
|
+
expect(completions).toContain('github');
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/completion.ts
CHANGED
package/src/doctor.ts
CHANGED
|
@@ -84,7 +84,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
84
84
|
}
|
|
85
85
|
if (status.running && !status.extensionConnected) {
|
|
86
86
|
issues.push(
|
|
87
|
-
'Daemon is running but the Chrome extension is not connected.\n' +
|
|
87
|
+
'Daemon is running but the Chrome/Chromium extension is not connected.\n' +
|
|
88
88
|
'Please install the opencli Browser Bridge extension:\n' +
|
|
89
89
|
' 1. Download from https://github.com/jackwener/opencli/releases\n' +
|
|
90
90
|
' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
|
package/src/electron-apps.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface ElectronAppEntry {
|
|
|
15
15
|
port: number;
|
|
16
16
|
/** macOS process name for detection via pgrep */
|
|
17
17
|
processName: string;
|
|
18
|
+
/** Candidate executable names inside Contents/MacOS/, tried in order */
|
|
19
|
+
executableNames?: string[];
|
|
18
20
|
/** macOS bundle ID for path discovery */
|
|
19
21
|
bundleId?: string;
|
|
20
22
|
/** Human-readable name for prompts */
|
|
@@ -30,7 +32,13 @@ export const builtinApps: Record<string, ElectronAppEntry> = {
|
|
|
30
32
|
notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' },
|
|
31
33
|
'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' },
|
|
32
34
|
'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' },
|
|
33
|
-
antigravity: {
|
|
35
|
+
antigravity: {
|
|
36
|
+
port: 9234,
|
|
37
|
+
processName: 'Antigravity',
|
|
38
|
+
executableNames: ['Electron', 'Antigravity'],
|
|
39
|
+
bundleId: 'dev.antigravity.app',
|
|
40
|
+
displayName: 'Antigravity',
|
|
41
|
+
},
|
|
34
42
|
chatgpt: { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' },
|
|
35
43
|
};
|
|
36
44
|
|
package/src/errors.ts
CHANGED
|
@@ -91,7 +91,7 @@ export class AuthRequiredError extends CliError {
|
|
|
91
91
|
super(
|
|
92
92
|
'AUTH_REQUIRED',
|
|
93
93
|
message ?? `Not logged in to ${domain}`,
|
|
94
|
-
`Please open Chrome and log in to https://${domain}`,
|
|
94
|
+
`Please open Chrome or Chromium and log in to https://${domain}`,
|
|
95
95
|
EXIT_CODES.NOPERM,
|
|
96
96
|
);
|
|
97
97
|
this.domain = domain;
|
package/src/execution.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { emitHook, type HookContext } from './hooks.js';
|
|
|
21
21
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
22
22
|
import { log } from './logger.js';
|
|
23
23
|
import { isElectronApp } from './electron-apps.js';
|
|
24
|
-
import { resolveElectronEndpoint } from './launcher.js';
|
|
24
|
+
import { probeCDP, resolveElectronEndpoint } from './launcher.js';
|
|
25
25
|
|
|
26
26
|
const _loadedModules = new Set<string>();
|
|
27
27
|
|
|
@@ -131,23 +131,6 @@ function ensureRequiredEnv(cmd: CliCommand): void {
|
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
* Check if the browser is already on the target domain, avoiding redundant navigation.
|
|
136
|
-
* Returns true if current page hostname matches the pre-nav URL hostname.
|
|
137
|
-
*/
|
|
138
|
-
async function isAlreadyOnDomain(page: IPage, targetUrl: string): Promise<boolean> {
|
|
139
|
-
if (!page.getCurrentUrl) return false;
|
|
140
|
-
try {
|
|
141
|
-
const currentUrl = await page.getCurrentUrl();
|
|
142
|
-
if (!currentUrl) return false;
|
|
143
|
-
const currentHost = new URL(currentUrl).hostname;
|
|
144
|
-
const targetHost = new URL(targetUrl).hostname;
|
|
145
|
-
return currentHost === targetHost;
|
|
146
|
-
} catch {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
134
|
export async function executeCommand(
|
|
152
135
|
cmd: CliCommand,
|
|
153
136
|
rawKwargs: CommandArgs,
|
|
@@ -156,6 +139,7 @@ export async function executeCommand(
|
|
|
156
139
|
let kwargs: CommandArgs;
|
|
157
140
|
try {
|
|
158
141
|
kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
|
|
142
|
+
cmd.validateArgs?.(kwargs);
|
|
159
143
|
} catch (err) {
|
|
160
144
|
if (err instanceof ArgumentError) throw err;
|
|
161
145
|
throw new ArgumentError(getErrorMessage(err));
|
|
@@ -175,8 +159,20 @@ export async function executeCommand(
|
|
|
175
159
|
let cdpEndpoint: string | undefined;
|
|
176
160
|
|
|
177
161
|
if (electron) {
|
|
178
|
-
// Electron apps:
|
|
179
|
-
|
|
162
|
+
// Electron apps: respect manual endpoint override, then try auto-detect
|
|
163
|
+
const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
164
|
+
if (manualEndpoint) {
|
|
165
|
+
const port = Number(new URL(manualEndpoint).port);
|
|
166
|
+
if (!await probeCDP(port)) {
|
|
167
|
+
throw new CommandExecutionError(
|
|
168
|
+
`CDP not reachable at ${manualEndpoint}`,
|
|
169
|
+
'Check that the app is running with --remote-debugging-port and the endpoint is correct.',
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
cdpEndpoint = manualEndpoint;
|
|
173
|
+
} else {
|
|
174
|
+
cdpEndpoint = await resolveElectronEndpoint(cmd.site);
|
|
175
|
+
}
|
|
180
176
|
} else {
|
|
181
177
|
// Browser Bridge: fail-fast when daemon is up but extension is missing.
|
|
182
178
|
// 300ms timeout avoids a full 2s wait on cold-start.
|
|
@@ -186,7 +182,7 @@ export async function executeCommand(
|
|
|
186
182
|
'Browser Bridge extension not connected',
|
|
187
183
|
'Install the Browser Bridge:\n' +
|
|
188
184
|
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
189
|
-
' 2. chrome://extensions → Developer Mode → Load unpacked\n' +
|
|
185
|
+
' 2. In Chrome or Chromium, open chrome://extensions → Developer Mode → Load unpacked\n' +
|
|
190
186
|
' Then run: opencli doctor',
|
|
191
187
|
);
|
|
192
188
|
}
|
|
@@ -197,15 +193,15 @@ export async function executeCommand(
|
|
|
197
193
|
result = await browserSession(BrowserFactory, async (page) => {
|
|
198
194
|
const preNavUrl = resolvePreNav(cmd);
|
|
199
195
|
if (preNavUrl) {
|
|
200
|
-
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
196
|
+
// Navigate directly — the extension's handleNavigate already has a fast-path
|
|
197
|
+
// that skips navigation if the tab is already at the target URL.
|
|
198
|
+
// This avoids an extra exec round-trip (getCurrentUrl) on first command and
|
|
199
|
+
// lets the extension create the automation window with the target URL directly
|
|
200
|
+
// instead of about:blank.
|
|
201
|
+
try {
|
|
202
|
+
await page.goto(preNavUrl);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (debug) log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
|
|
209
205
|
}
|
|
210
206
|
}
|
|
211
207
|
return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
|
package/src/explore.ts
CHANGED
|
@@ -386,7 +386,7 @@ export async function exploreUrl(
|
|
|
386
386
|
const clicks = await page.evaluate(INTERACT_FUZZ_JS);
|
|
387
387
|
await page.wait(2); // wait for XHRs to settle
|
|
388
388
|
} catch (e) {
|
|
389
|
-
log.
|
|
389
|
+
log.verbose(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
|
package/src/launcher.test.ts
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import type { ElectronAppEntry } from './electron-apps.js';
|
|
3
|
+
import { detectProcess, discoverAppPath, launchDetachedApp, launchElectronApp, probeCDP, resolveExecutableCandidates } from './launcher.js';
|
|
4
|
+
|
|
5
|
+
interface MockChildProcess {
|
|
6
|
+
once: ReturnType<typeof vi.fn>;
|
|
7
|
+
off: ReturnType<typeof vi.fn>;
|
|
8
|
+
unref: ReturnType<typeof vi.fn>;
|
|
9
|
+
emit: (event: string, value?: unknown) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createMockChildProcess(): MockChildProcess {
|
|
13
|
+
const listeners = new Map<string, Array<(value?: unknown) => void>>();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
once: vi.fn((event: string, handler: (value?: unknown) => void) => {
|
|
17
|
+
listeners.set(event, [...(listeners.get(event) ?? []), handler]);
|
|
18
|
+
}),
|
|
19
|
+
off: vi.fn((event: string, handler: (value?: unknown) => void) => {
|
|
20
|
+
listeners.set(event, (listeners.get(event) ?? []).filter((listener) => listener !== handler));
|
|
21
|
+
}),
|
|
22
|
+
unref: vi.fn(),
|
|
23
|
+
emit: (event: string, value?: unknown) => {
|
|
24
|
+
for (const listener of listeners.get(event) ?? []) listener(value);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
3
28
|
|
|
4
29
|
vi.mock('node:child_process', () => ({
|
|
5
30
|
execFileSync: vi.fn(),
|
|
6
|
-
spawn: vi.fn(
|
|
7
|
-
unref: vi.fn(),
|
|
8
|
-
pid: 12345,
|
|
9
|
-
on: vi.fn(),
|
|
10
|
-
})),
|
|
31
|
+
spawn: vi.fn(),
|
|
11
32
|
}));
|
|
12
33
|
|
|
13
34
|
const cp = vi.mocked(await import('node:child_process'));
|
|
@@ -34,7 +55,7 @@ describe('detectProcess', () => {
|
|
|
34
55
|
expect(result).toBe(false);
|
|
35
56
|
});
|
|
36
57
|
|
|
37
|
-
it('returns true when pgrep finds a process', () => {
|
|
58
|
+
it.skipIf(process.platform === 'win32')('returns true when pgrep finds a process', () => {
|
|
38
59
|
cp.execFileSync.mockReturnValue('12345\n');
|
|
39
60
|
const result = detectProcess('Cursor');
|
|
40
61
|
expect(result).toBe(true);
|
|
@@ -65,3 +86,96 @@ describe('discoverAppPath', () => {
|
|
|
65
86
|
expect(result).toBeNull();
|
|
66
87
|
});
|
|
67
88
|
});
|
|
89
|
+
|
|
90
|
+
describe('launchDetachedApp', () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.restoreAllMocks();
|
|
93
|
+
cp.spawn.mockReset();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('unrefs the process after spawn succeeds', async () => {
|
|
97
|
+
const child = createMockChildProcess();
|
|
98
|
+
cp.spawn.mockImplementation(() => {
|
|
99
|
+
queueMicrotask(() => child.emit('spawn'));
|
|
100
|
+
return child as unknown as ReturnType<typeof cp.spawn>;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await expect(launchDetachedApp('/Applications/Antigravity.app/Contents/MacOS/Antigravity', ['--remote-debugging-port=9234'], 'Antigravity'))
|
|
104
|
+
.resolves
|
|
105
|
+
.toBeUndefined();
|
|
106
|
+
expect(child.unref).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('converts ENOENT into a controlled launch error', async () => {
|
|
110
|
+
const child = createMockChildProcess();
|
|
111
|
+
cp.spawn.mockImplementation(() => {
|
|
112
|
+
queueMicrotask(() => child.emit('error', Object.assign(new Error('missing binary'), { code: 'ENOENT' })));
|
|
113
|
+
return child as unknown as ReturnType<typeof cp.spawn>;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await expect(launchDetachedApp('/Applications/Antigravity.app/Contents/MacOS/Antigravity', ['--remote-debugging-port=9234'], 'Antigravity'))
|
|
117
|
+
.rejects
|
|
118
|
+
.toThrow('Could not launch Antigravity');
|
|
119
|
+
expect(child.unref).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('resolveExecutableCandidates', () => {
|
|
124
|
+
it('prefers explicit executable candidates over processName', () => {
|
|
125
|
+
const app: ElectronAppEntry = {
|
|
126
|
+
port: 9234,
|
|
127
|
+
processName: 'Antigravity',
|
|
128
|
+
executableNames: ['Electron', 'Antigravity'],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
expect(resolveExecutableCandidates('/Applications/Antigravity.app', app)).toEqual([
|
|
132
|
+
'/Applications/Antigravity.app/Contents/MacOS/Electron',
|
|
133
|
+
'/Applications/Antigravity.app/Contents/MacOS/Antigravity',
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('launchElectronApp', () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
vi.restoreAllMocks();
|
|
141
|
+
cp.spawn.mockReset();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('falls back to the next executable candidate when the first is missing', async () => {
|
|
145
|
+
const firstChild = createMockChildProcess();
|
|
146
|
+
const secondChild = createMockChildProcess();
|
|
147
|
+
const app: ElectronAppEntry = {
|
|
148
|
+
port: 9234,
|
|
149
|
+
processName: 'Antigravity',
|
|
150
|
+
executableNames: ['Electron', 'Antigravity'],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
cp.spawn
|
|
154
|
+
.mockImplementationOnce(() => {
|
|
155
|
+
queueMicrotask(() => firstChild.emit('error', Object.assign(new Error('missing binary'), { code: 'ENOENT' })));
|
|
156
|
+
return firstChild as unknown as ReturnType<typeof cp.spawn>;
|
|
157
|
+
})
|
|
158
|
+
.mockImplementationOnce(() => {
|
|
159
|
+
queueMicrotask(() => secondChild.emit('spawn'));
|
|
160
|
+
return secondChild as unknown as ReturnType<typeof cp.spawn>;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await expect(
|
|
164
|
+
launchElectronApp('/Applications/Antigravity.app', app, ['--remote-debugging-port=9234'], 'Antigravity'),
|
|
165
|
+
).resolves.toBeUndefined();
|
|
166
|
+
|
|
167
|
+
expect(cp.spawn).toHaveBeenNthCalledWith(
|
|
168
|
+
1,
|
|
169
|
+
'/Applications/Antigravity.app/Contents/MacOS/Electron',
|
|
170
|
+
['--remote-debugging-port=9234'],
|
|
171
|
+
{ detached: true, stdio: 'ignore' },
|
|
172
|
+
);
|
|
173
|
+
expect(cp.spawn).toHaveBeenNthCalledWith(
|
|
174
|
+
2,
|
|
175
|
+
'/Applications/Antigravity.app/Contents/MacOS/Antigravity',
|
|
176
|
+
['--remote-debugging-port=9234'],
|
|
177
|
+
{ detached: true, stdio: 'ignore' },
|
|
178
|
+
);
|
|
179
|
+
expect(secondChild.unref).toHaveBeenCalledTimes(1);
|
|
180
|
+
});
|
|
181
|
+
});
|