@jackwener/opencli 1.4.1 → 1.5.1
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/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +21 -1
- package/README.md +35 -6
- package/README.zh-CN.md +12 -5
- package/SKILL.md +2 -0
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/cdp.js +5 -0
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/browser/page.d.ts +3 -0
- package/dist/browser/page.js +24 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1567 -108
- package/dist/cli.js +68 -6
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/bluesky/feeds.yaml +29 -0
- package/dist/clis/bluesky/followers.yaml +33 -0
- package/dist/clis/bluesky/following.yaml +33 -0
- package/dist/clis/bluesky/profile.yaml +27 -0
- package/dist/clis/bluesky/search.yaml +34 -0
- package/dist/clis/bluesky/starter-packs.yaml +34 -0
- package/dist/clis/bluesky/thread.yaml +32 -0
- package/dist/clis/bluesky/trending.yaml +27 -0
- package/dist/clis/bluesky/user.yaml +34 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +2 -1
- package/dist/clis/twitter/search.test.js +2 -0
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/twitter/trending.js +29 -61
- package/dist/clis/v2ex/hot.yaml +17 -3
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +179 -47
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +131 -0
- package/dist/clis/xiaohongshu/search.d.ts +8 -1
- package/dist/clis/xiaohongshu/search.js +20 -1
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
- package/dist/clis/xiaohongshu/search.test.js +32 -1
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +48 -42
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +11 -4
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/engine.test.js +42 -0
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +17 -34
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +66 -8
- package/dist/execution.test.d.ts +1 -0
- package/dist/execution.test.js +40 -0
- package/dist/external.js +6 -1
- package/dist/hooks.js +2 -0
- package/dist/main.js +6 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin-scaffold.d.ts +28 -0
- package/dist/plugin-scaffold.js +142 -0
- package/dist/plugin-scaffold.test.d.ts +4 -0
- package/dist/plugin-scaffold.test.js +83 -0
- package/dist/plugin.d.ts +82 -11
- package/dist/plugin.js +870 -84
- package/dist/plugin.test.js +1032 -17
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +2 -2
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +3 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/bluesky.md +53 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +6 -2
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +97 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/package.json +1 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +2 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +8 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/browser/page.ts +24 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +69 -6
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/bluesky/feeds.yaml +29 -0
- package/src/clis/bluesky/followers.yaml +33 -0
- package/src/clis/bluesky/following.yaml +33 -0
- package/src/clis/bluesky/profile.yaml +27 -0
- package/src/clis/bluesky/search.yaml +34 -0
- package/src/clis/bluesky/starter-packs.yaml +34 -0
- package/src/clis/bluesky/thread.yaml +32 -0
- package/src/clis/bluesky/trending.yaml +27 -0
- package/src/clis/bluesky/user.yaml +34 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +2 -0
- package/src/clis/twitter/search.ts +3 -1
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/twitter/trending.ts +29 -77
- package/src/clis/v2ex/hot.yaml +17 -3
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +151 -0
- package/src/clis/xiaohongshu/publish.ts +206 -54
- package/src/clis/xiaohongshu/search.test.ts +39 -1
- package/src/clis/xiaohongshu/search.ts +19 -1
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +49 -48
- package/src/doctor.ts +15 -5
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/engine.test.ts +38 -0
- package/src/errors.ts +26 -63
- package/src/execution.test.ts +47 -0
- package/src/execution.ts +67 -9
- package/src/external.ts +6 -1
- package/src/hooks.ts +1 -0
- package/src/main.ts +7 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin-scaffold.test.ts +98 -0
- package/src/plugin-scaffold.ts +170 -0
- package/src/plugin.test.ts +1104 -17
- package/src/plugin.ts +1101 -86
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +3 -3
- package/src/serialization.ts +4 -0
- package/src/types.ts +3 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +1 -1
- package/tests/e2e/browser-public.test.ts +62 -4
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +10 -0
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
package/src/registry.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface CliCommand {
|
|
|
46
46
|
source?: string;
|
|
47
47
|
footerExtra?: (kwargs: CommandArgs) => string | undefined;
|
|
48
48
|
requiredEnv?: RequiredEnv[];
|
|
49
|
+
/** Deprecation note shown in help / execution warnings. */
|
|
50
|
+
deprecated?: boolean | string;
|
|
51
|
+
/** Preferred replacement command, if any. */
|
|
52
|
+
replacedBy?: string;
|
|
49
53
|
/**
|
|
50
54
|
* Control pre-navigation for cookie/header context before command execution.
|
|
51
55
|
*
|
|
@@ -95,6 +99,8 @@ export function cli(opts: CliOptions): CliCommand {
|
|
|
95
99
|
timeoutSeconds: opts.timeoutSeconds,
|
|
96
100
|
footerExtra: opts.footerExtra,
|
|
97
101
|
requiredEnv: opts.requiredEnv,
|
|
102
|
+
deprecated: opts.deprecated,
|
|
103
|
+
replacedBy: opts.replacedBy,
|
|
98
104
|
navigateBefore: opts.navigateBefore,
|
|
99
105
|
};
|
|
100
106
|
|
|
@@ -118,4 +124,3 @@ export function strategyLabel(cmd: CliCommand): string {
|
|
|
118
124
|
export function registerCommand(cmd: CliCommand): void {
|
|
119
125
|
_registry.set(fullName(cmd), cmd);
|
|
120
126
|
}
|
|
121
|
-
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { detectRuntime, getRuntimeVersion, getRuntimeLabel } from './runtime-detect.js';
|
|
3
|
+
|
|
4
|
+
describe('runtime-detect', () => {
|
|
5
|
+
it('detectRuntime returns a valid runtime string', () => {
|
|
6
|
+
const rt = detectRuntime();
|
|
7
|
+
expect(['bun', 'node']).toContain(rt);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('getRuntimeVersion returns a non-empty version string', () => {
|
|
11
|
+
const ver = getRuntimeVersion();
|
|
12
|
+
expect(typeof ver).toBe('string');
|
|
13
|
+
expect(ver.length).toBeGreaterThan(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('getRuntimeLabel returns "<runtime> <version>" format', () => {
|
|
17
|
+
const label = getRuntimeLabel();
|
|
18
|
+
expect(label).toMatch(/^(bun|node) .+$/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('detects the current environment correctly', () => {
|
|
22
|
+
const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
|
23
|
+
const rt = detectRuntime();
|
|
24
|
+
if (isBun) {
|
|
25
|
+
expect(rt).toBe('bun');
|
|
26
|
+
} else {
|
|
27
|
+
expect(rt).toBe('node');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime detection — identify whether opencli is running under Node.js or Bun.
|
|
3
|
+
*
|
|
4
|
+
* Bun injects `globalThis.Bun` at startup, making detection trivial.
|
|
5
|
+
* This module centralises the check so other code can adapt behaviour
|
|
6
|
+
* (e.g. logging, diagnostics) without littering runtime sniffing everywhere.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Runtime = 'bun' | 'node';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect the current JavaScript runtime.
|
|
13
|
+
*/
|
|
14
|
+
export function detectRuntime(): Runtime {
|
|
15
|
+
// Bun always exposes globalThis.Bun (including Bun.version)
|
|
16
|
+
if (typeof (globalThis as any).Bun !== 'undefined') return 'bun';
|
|
17
|
+
return 'node';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return a human-readable version string for the current runtime.
|
|
22
|
+
* Examples: "v22.13.0" (Node), "1.1.42" (Bun)
|
|
23
|
+
*/
|
|
24
|
+
export function getRuntimeVersion(): string {
|
|
25
|
+
if (detectRuntime() === 'bun') {
|
|
26
|
+
return (globalThis as any).Bun.version as string;
|
|
27
|
+
}
|
|
28
|
+
return process.version; // e.g. "v22.13.0"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Return a combined label like "node v22.13.0" or "bun 1.1.42".
|
|
33
|
+
*/
|
|
34
|
+
export function getRuntimeLabel(): string {
|
|
35
|
+
return `${detectRuntime()} ${getRuntimeVersion()}`;
|
|
36
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { TimeoutError } from './errors.js';
|
|
|
7
7
|
* Uses CDPBridge when OPENCLI_CDP_ENDPOINT is set, otherwise BrowserBridge.
|
|
8
8
|
*/
|
|
9
9
|
export function getBrowserFactory(): new () => IBrowserFactory {
|
|
10
|
-
return
|
|
10
|
+
return process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function parseEnvTimeout(envVar: string, fallback: number): number {
|
|
@@ -30,11 +30,11 @@ export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_
|
|
|
30
30
|
*/
|
|
31
31
|
export async function runWithTimeout<T>(
|
|
32
32
|
promise: Promise<T>,
|
|
33
|
-
opts: { timeout: number; label?: string },
|
|
33
|
+
opts: { timeout: number; label?: string; hint?: string },
|
|
34
34
|
): Promise<T> {
|
|
35
35
|
const label = opts.label ?? 'Operation';
|
|
36
36
|
return withTimeoutMs(promise, opts.timeout * 1000,
|
|
37
|
-
() => new TimeoutError(label, opts.timeout));
|
|
37
|
+
() => new TimeoutError(label, opts.timeout, opts.hint));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
package/src/serialization.ts
CHANGED
|
@@ -45,6 +45,8 @@ export function serializeCommand(cmd: CliCommand) {
|
|
|
45
45
|
args: cmd.args.map(serializeArg),
|
|
46
46
|
columns: cmd.columns ?? [],
|
|
47
47
|
domain: cmd.domain ?? null,
|
|
48
|
+
deprecated: cmd.deprecated ?? null,
|
|
49
|
+
replacedBy: cmd.replacedBy ?? null,
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -73,6 +75,8 @@ export function formatRegistryHelpText(cmd: CliCommand): string {
|
|
|
73
75
|
meta.push(`Strategy: ${strategyLabel(cmd)}`);
|
|
74
76
|
meta.push(`Browser: ${cmd.browser ? 'yes' : 'no'}`);
|
|
75
77
|
if (cmd.domain) meta.push(`Domain: ${cmd.domain}`);
|
|
78
|
+
if (cmd.deprecated) meta.push(`Deprecated: ${typeof cmd.deprecated === 'string' ? cmd.deprecated : 'yes'}`);
|
|
79
|
+
if (cmd.replacedBy) meta.push(`Use instead: ${cmd.replacedBy}`);
|
|
76
80
|
lines.push(meta.join(' | '));
|
|
77
81
|
if (cmd.columns?.length) lines.push(`Output columns: ${cmd.columns.join(', ')}`);
|
|
78
82
|
return '\n' + lines.join('\n') + '\n';
|
package/src/types.ts
CHANGED
|
@@ -65,4 +65,7 @@ export interface IPage {
|
|
|
65
65
|
installInterceptor(pattern: string): Promise<void>;
|
|
66
66
|
getInterceptedRequests(): Promise<any[]>;
|
|
67
67
|
screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
68
|
+
closeWindow?(): Promise<void>;
|
|
69
|
+
/** Returns the current page URL, or null if unavailable. */
|
|
70
|
+
getCurrentUrl?(): Promise<string | null>;
|
|
68
71
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking update checker.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: register exit-hook + kick-off-background-fetch
|
|
5
|
+
* - On startup: kick off background fetch (non-blocking)
|
|
6
|
+
* - On process exit: read cache, print notice if newer version exists
|
|
7
|
+
* - Check interval: 24 hours
|
|
8
|
+
* - Notice appears AFTER command output, not before (same as npm/gh/yarn)
|
|
9
|
+
* - Never delays or blocks the CLI command
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { PKG_VERSION } from './version.js';
|
|
17
|
+
|
|
18
|
+
const CACHE_DIR = path.join(os.homedir(), '.opencli');
|
|
19
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json');
|
|
20
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
21
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@jackwener/opencli/latest';
|
|
22
|
+
|
|
23
|
+
interface UpdateCache {
|
|
24
|
+
lastCheck: number;
|
|
25
|
+
latestVersion: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Read cache once at module load — shared by both exported functions
|
|
29
|
+
const _cache: UpdateCache | null = (() => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8')) as UpdateCache;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
function writeCache(latestVersion: string): void {
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
40
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ lastCheck: Date.now(), latestVersion }), 'utf-8');
|
|
41
|
+
} catch {
|
|
42
|
+
// Best-effort; never fail
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Compare semver strings. Returns true if `a` is strictly newer than `b`. */
|
|
47
|
+
function isNewer(a: string, b: string): boolean {
|
|
48
|
+
const parse = (v: string) => v.replace(/^v/, '').split('-')[0].split('.').map(Number);
|
|
49
|
+
const pa = parse(a);
|
|
50
|
+
const pb = parse(b);
|
|
51
|
+
if (pa.some(isNaN) || pb.some(isNaN)) return false;
|
|
52
|
+
const [aMaj, aMin, aPat] = pa;
|
|
53
|
+
const [bMaj, bMin, bPat] = pb;
|
|
54
|
+
if (aMaj !== bMaj) return aMaj > bMaj;
|
|
55
|
+
if (aMin !== bMin) return aMin > bMin;
|
|
56
|
+
return aPat > bPat;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isCI(): boolean {
|
|
60
|
+
return !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register a process exit hook that prints an update notice if a newer
|
|
65
|
+
* version was found on the last background check.
|
|
66
|
+
* Notice appears after command output — same pattern as npm/gh/yarn.
|
|
67
|
+
* Skipped during --get-completions to avoid polluting shell completion output.
|
|
68
|
+
*/
|
|
69
|
+
export function registerUpdateNoticeOnExit(): void {
|
|
70
|
+
if (isCI()) return;
|
|
71
|
+
if (process.argv.includes('--get-completions')) return;
|
|
72
|
+
|
|
73
|
+
process.on('exit', (code) => {
|
|
74
|
+
if (code !== 0) return; // Don't show update notice on error exit
|
|
75
|
+
if (!_cache) return;
|
|
76
|
+
if (!isNewer(_cache.latestVersion, PKG_VERSION)) return;
|
|
77
|
+
try {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
chalk.yellow(`\n Update available: v${PKG_VERSION} → v${_cache.latestVersion}\n`) +
|
|
80
|
+
chalk.dim(` Run: npm install -g @jackwener/opencli\n\n`),
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore broken pipe (stderr closed before process exits)
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Kick off a background fetch to npm registry. Writes to cache for next run.
|
|
90
|
+
* Fully non-blocking — never awaited.
|
|
91
|
+
*/
|
|
92
|
+
export function checkForUpdateBackground(): void {
|
|
93
|
+
if (isCI()) return;
|
|
94
|
+
if (_cache && Date.now() - _cache.lastCheck < CHECK_INTERVAL_MS) return;
|
|
95
|
+
|
|
96
|
+
void (async () => {
|
|
97
|
+
try {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
100
|
+
const res = await fetch(NPM_REGISTRY_URL, {
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
headers: { 'User-Agent': `opencli/${PKG_VERSION}` },
|
|
103
|
+
});
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
if (!res.ok) return;
|
|
106
|
+
const data = await res.json() as { version?: string };
|
|
107
|
+
if (typeof data.version === 'string') {
|
|
108
|
+
writeCache(data.version);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Network error: silently skip, try again next run
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
async function loadModule() {
|
|
4
|
+
return import('./clis/weixin/download.js');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
describe('weixin publish time extraction', () => {
|
|
8
|
+
it('prefers publish_time text over create_time-like date strings', async () => {
|
|
9
|
+
const mod = await loadModule();
|
|
10
|
+
|
|
11
|
+
expect(mod.extractWechatPublishTime(
|
|
12
|
+
'2026年3月24日 22:38',
|
|
13
|
+
'var create_time = "2026年3月24日 22:38";',
|
|
14
|
+
)).toBe('2026年3月24日 22:38');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('falls back to unix timestamp create_time values', async () => {
|
|
18
|
+
const mod = await loadModule();
|
|
19
|
+
|
|
20
|
+
expect(mod.extractWechatPublishTime(
|
|
21
|
+
'',
|
|
22
|
+
'var create_time = "1711291080";',
|
|
23
|
+
)).toBe('2024-03-24 22:38:00');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rejects malformed create_time values', async () => {
|
|
27
|
+
const mod = await loadModule();
|
|
28
|
+
|
|
29
|
+
expect(mod.extractWechatPublishTime(
|
|
30
|
+
'',
|
|
31
|
+
'var create_time = "2026年3月24日 22:38";',
|
|
32
|
+
)).toBe('');
|
|
33
|
+
expect(mod.extractWechatPublishTime(
|
|
34
|
+
'',
|
|
35
|
+
'var create_time = "1711291080abc";',
|
|
36
|
+
)).toBe('');
|
|
37
|
+
expect(mod.extractWechatPublishTime(
|
|
38
|
+
'',
|
|
39
|
+
'var create_time = "17112910800";',
|
|
40
|
+
)).toBe('');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('builds a self-contained browser helper that matches fallback behavior', async () => {
|
|
44
|
+
const mod = await loadModule();
|
|
45
|
+
|
|
46
|
+
const extractInPage = eval(mod.buildExtractWechatPublishTimeJs()) as (publishTimeText: string, htmlStr: string) => string;
|
|
47
|
+
|
|
48
|
+
expect(extractInPage(
|
|
49
|
+
'',
|
|
50
|
+
'var create_time = "1711291080";',
|
|
51
|
+
)).toBe('2024-03-24 22:38:00');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('browser helper still prefers DOM publish_time text', async () => {
|
|
55
|
+
const mod = await loadModule();
|
|
56
|
+
|
|
57
|
+
const extractInPage = eval(mod.buildExtractWechatPublishTimeJs()) as (publishTimeText: string, htmlStr: string) => string;
|
|
58
|
+
|
|
59
|
+
expect(extractInPage(
|
|
60
|
+
'2026年3月24日 22:38',
|
|
61
|
+
'var create_time = "1711291080";',
|
|
62
|
+
)).toBe('2026年3月24日 22:38');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from './registry.js';
|
|
3
|
+
import { fetchPrivateApi } from './clis/weread/utils.js';
|
|
4
|
+
import './clis/weread/shelf.js';
|
|
5
|
+
|
|
6
|
+
describe('weread private API regression', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('uses browser cookies and Node fetch for private API requests', async () => {
|
|
12
|
+
const mockPage = {
|
|
13
|
+
getCookies: vi.fn()
|
|
14
|
+
.mockResolvedValueOnce([
|
|
15
|
+
{ name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
|
|
16
|
+
{ name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
|
|
17
|
+
]),
|
|
18
|
+
evaluate: vi.fn(),
|
|
19
|
+
} as any;
|
|
20
|
+
|
|
21
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
json: () => Promise.resolve({ title: 'Test Book', errcode: 0 }),
|
|
25
|
+
});
|
|
26
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
27
|
+
|
|
28
|
+
const result = await fetchPrivateApi(mockPage, '/book/info', { bookId: '123' });
|
|
29
|
+
|
|
30
|
+
expect(result.title).toBe('Test Book');
|
|
31
|
+
expect(mockPage.getCookies).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(mockPage.getCookies).toHaveBeenCalledWith({ url: 'https://i.weread.qq.com/book/info?bookId=123' });
|
|
33
|
+
expect(mockPage.evaluate).not.toHaveBeenCalled();
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
+
'https://i.weread.qq.com/book/info?bookId=123',
|
|
36
|
+
expect.objectContaining({
|
|
37
|
+
headers: expect.objectContaining({
|
|
38
|
+
Cookie: 'wr_name=alice; wr_vid=vid123',
|
|
39
|
+
}),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('maps unauthenticated private API responses to AUTH_REQUIRED', async () => {
|
|
45
|
+
const mockPage = {
|
|
46
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
47
|
+
evaluate: vi.fn(),
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
51
|
+
ok: false,
|
|
52
|
+
status: 401,
|
|
53
|
+
json: () => Promise.resolve({ errcode: -2010, errmsg: '用户不存在' }),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('Not logged in');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('maps non-auth API errors to API_ERROR', async () => {
|
|
60
|
+
const mockPage = {
|
|
61
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
62
|
+
evaluate: vi.fn(),
|
|
63
|
+
} as any;
|
|
64
|
+
|
|
65
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
66
|
+
ok: true,
|
|
67
|
+
status: 200,
|
|
68
|
+
json: () => Promise.resolve({ errcode: -1, errmsg: 'unknown error' }),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('unknown error');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('maps non-401 HTTP failures to FETCH_ERROR', async () => {
|
|
75
|
+
const mockPage = {
|
|
76
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
77
|
+
evaluate: vi.fn(),
|
|
78
|
+
} as any;
|
|
79
|
+
|
|
80
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
81
|
+
ok: false,
|
|
82
|
+
status: 403,
|
|
83
|
+
json: () => Promise.resolve({ errmsg: 'forbidden' }),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('HTTP 403');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('maps invalid JSON to PARSE_ERROR', async () => {
|
|
90
|
+
const mockPage = {
|
|
91
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
92
|
+
evaluate: vi.fn(),
|
|
93
|
+
} as any;
|
|
94
|
+
|
|
95
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
96
|
+
ok: true,
|
|
97
|
+
status: 200,
|
|
98
|
+
json: () => Promise.reject(new SyntaxError('Unexpected token <')),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('Invalid JSON');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('routes weread shelf through the private API helper path', async () => {
|
|
105
|
+
const command = getRegistry().get('weread/shelf');
|
|
106
|
+
expect(command?.func).toBeTypeOf('function');
|
|
107
|
+
|
|
108
|
+
const mockPage = {
|
|
109
|
+
getCookies: vi.fn()
|
|
110
|
+
.mockResolvedValueOnce([
|
|
111
|
+
{ name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
|
|
112
|
+
{ name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
|
|
113
|
+
]),
|
|
114
|
+
evaluate: vi.fn(),
|
|
115
|
+
} as any;
|
|
116
|
+
|
|
117
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
118
|
+
ok: true,
|
|
119
|
+
status: 200,
|
|
120
|
+
json: () => Promise.resolve({
|
|
121
|
+
books: [{
|
|
122
|
+
title: 'Deep Work',
|
|
123
|
+
author: 'Cal Newport',
|
|
124
|
+
readingProgress: 42,
|
|
125
|
+
bookId: 'abc123',
|
|
126
|
+
}],
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
130
|
+
|
|
131
|
+
const result = await command!.func!(mockPage, { limit: 1 });
|
|
132
|
+
|
|
133
|
+
expect(mockPage.evaluate).not.toHaveBeenCalled();
|
|
134
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
135
|
+
'https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0',
|
|
136
|
+
expect.any(Object),
|
|
137
|
+
);
|
|
138
|
+
expect(mockPage.getCookies).toHaveBeenCalledWith({
|
|
139
|
+
url: 'https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0',
|
|
140
|
+
});
|
|
141
|
+
expect(result).toEqual([
|
|
142
|
+
{
|
|
143
|
+
title: 'Deep Work',
|
|
144
|
+
author: 'Cal Newport',
|
|
145
|
+
progress: '42%',
|
|
146
|
+
bookId: 'abc123',
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
});
|
package/src/yaml-schema.ts
CHANGED
|
@@ -26,3 +26,23 @@ export interface YamlCliDefinition {
|
|
|
26
26
|
timeout?: number;
|
|
27
27
|
navigateBefore?: boolean | string;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
import type { Arg } from './registry.js';
|
|
31
|
+
|
|
32
|
+
/** Convert YAML args definition to the internal Arg[] format. */
|
|
33
|
+
export function parseYamlArgs(args: Record<string, YamlArgDefinition> | undefined): Arg[] {
|
|
34
|
+
if (!args || typeof args !== 'object') return [];
|
|
35
|
+
const result: Arg[] = [];
|
|
36
|
+
for (const [argName, argDef] of Object.entries(args)) {
|
|
37
|
+
result.push({
|
|
38
|
+
name: argName,
|
|
39
|
+
type: argDef?.type ?? 'str',
|
|
40
|
+
default: argDef?.default,
|
|
41
|
+
required: argDef?.required ?? false,
|
|
42
|
+
positional: argDef?.positional ?? false,
|
|
43
|
+
help: argDef?.description ?? argDef?.help ?? '',
|
|
44
|
+
choices: argDef?.choices,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
@@ -80,30 +80,34 @@ describe('login-required commands — graceful failure', () => {
|
|
|
80
80
|
}, 60_000);
|
|
81
81
|
|
|
82
82
|
// ── linux-do (requires login — all endpoints need authentication) ──
|
|
83
|
-
it('linux-do
|
|
84
|
-
await expectGracefulAuthFailure(['linux-do', '
|
|
85
|
-
}, 60_000);
|
|
86
|
-
|
|
87
|
-
it('linux-do latest fails gracefully without login', async () => {
|
|
88
|
-
await expectGracefulAuthFailure(['linux-do', 'latest', '--limit', '3', '-f', 'json'], 'linux-do latest');
|
|
83
|
+
it('linux-do feed fails gracefully without login', async () => {
|
|
84
|
+
await expectGracefulAuthFailure(['linux-do', 'feed', '--limit', '3', '-f', 'json'], 'linux-do feed');
|
|
89
85
|
}, 60_000);
|
|
90
86
|
|
|
91
87
|
it('linux-do categories fails gracefully without login', async () => {
|
|
92
88
|
await expectGracefulAuthFailure(['linux-do', 'categories', '--limit', '3', '-f', 'json'], 'linux-do categories');
|
|
93
89
|
}, 60_000);
|
|
94
90
|
|
|
95
|
-
it('linux-do
|
|
96
|
-
await expectGracefulAuthFailure(['linux-do', '
|
|
91
|
+
it('linux-do tags fails gracefully without login', async () => {
|
|
92
|
+
await expectGracefulAuthFailure(['linux-do', 'tags', '--limit', '3', '-f', 'json'], 'linux-do tags');
|
|
97
93
|
}, 60_000);
|
|
98
94
|
|
|
99
95
|
it('linux-do topic fails gracefully without login', async () => {
|
|
100
|
-
await expectGracefulAuthFailure(['linux-do', 'topic', '
|
|
96
|
+
await expectGracefulAuthFailure(['linux-do', 'topic', '1', '-f', 'json'], 'linux-do topic');
|
|
101
97
|
}, 60_000);
|
|
102
98
|
|
|
103
99
|
it('linux-do search fails gracefully without login', async () => {
|
|
104
100
|
await expectGracefulAuthFailure(['linux-do', 'search', 'test', '--limit', '3', '-f', 'json'], 'linux-do search');
|
|
105
101
|
}, 60_000);
|
|
106
102
|
|
|
103
|
+
it('linux-do user-topics fails gracefully without login', async () => {
|
|
104
|
+
await expectGracefulAuthFailure(['linux-do', 'user-topics', 'test', '--limit', '3', '-f', 'json'], 'linux-do user-topics');
|
|
105
|
+
}, 60_000);
|
|
106
|
+
|
|
107
|
+
it('linux-do user-posts fails gracefully without login', async () => {
|
|
108
|
+
await expectGracefulAuthFailure(['linux-do', 'user-posts', 'test', '--limit', '3', '-f', 'json'], 'linux-do user-posts');
|
|
109
|
+
}, 60_000);
|
|
110
|
+
|
|
107
111
|
// ── xiaohongshu (requires login) ──
|
|
108
112
|
it('xiaohongshu feed fails gracefully without login', async () => {
|
|
109
113
|
await expectGracefulAuthFailure(['xiaohongshu', 'feed', '--limit', '3', '-f', 'json'], 'xiaohongshu feed');
|
|
@@ -156,7 +156,7 @@ describe('browser extended public-data commands E2E', () => {
|
|
|
156
156
|
|
|
157
157
|
// ── yahoo-finance ──
|
|
158
158
|
it('yahoo-finance quote returns stock data', async () => {
|
|
159
|
-
const data = await tryBrowserCommand(['yahoo-finance', 'quote', '
|
|
159
|
+
const data = await tryBrowserCommand(['yahoo-finance', 'quote', 'AAPL', '-f', 'json']);
|
|
160
160
|
expectDataOrSkip(data, 'yahoo-finance quote');
|
|
161
161
|
}, 60_000);
|
|
162
162
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* E2E tests for core browser commands (bilibili, zhihu, v2ex).
|
|
2
|
+
* E2E tests for core browser commands (bilibili, zhihu, v2ex, IMDb).
|
|
3
3
|
* These use OPENCLI_HEADLESS=1 to launch a headless Chromium.
|
|
4
4
|
*
|
|
5
5
|
* NOTE: Some sites may block headless browsers with bot detection.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import { runCli, parseJsonOutput } from './helpers.js';
|
|
10
|
+
import { runCli, parseJsonOutput, type CliResult } from './helpers.js';
|
|
11
11
|
|
|
12
12
|
async function tryBrowserCommand(args: string[]): Promise<any[] | null> {
|
|
13
13
|
const { stdout, code } = await runCli(args, { timeout: 60_000 });
|
|
@@ -28,13 +28,47 @@ function expectDataOrSkip(data: any[] | null, label: string) {
|
|
|
28
28
|
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function isImdbChallenge(result: CliResult): boolean {
|
|
32
|
+
const text = `${result.stderr}\n${result.stdout}`;
|
|
33
|
+
return /IMDb blocked this request|Robot Check|Are you a robot|verify that you are human|captcha/i.test(text);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isBrowserBridgeUnavailable(result: CliResult): boolean {
|
|
37
|
+
const text = `${result.stderr}\n${result.stdout}`;
|
|
38
|
+
return /Browser Bridge not connected|Extension.*not connected|not connected.*extension/i.test(text);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function expectImdbDataOrChallengeSkip(args: string[], label: string): Promise<any[] | null> {
|
|
42
|
+
const result = await runCli(args, { timeout: 60_000 });
|
|
43
|
+
if (result.code !== 0) {
|
|
44
|
+
if (isImdbChallenge(result)) {
|
|
45
|
+
console.warn(`${label}: skipped — IMDb challenge page detected`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (isBrowserBridgeUnavailable(result)) {
|
|
49
|
+
console.warn(`${label}: skipped — Browser Bridge extension is unavailable in this environment`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`${label} failed:\n${result.stderr || result.stdout}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const data = parseJsonOutput(result.stdout);
|
|
56
|
+
if (!Array.isArray(data)) {
|
|
57
|
+
throw new Error(`${label} returned non-array JSON:\n${result.stdout.slice(0, 500)}`);
|
|
58
|
+
}
|
|
59
|
+
if (data.length === 0) {
|
|
60
|
+
throw new Error(`${label} returned an empty result`);
|
|
61
|
+
}
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
describe('browser public-data commands E2E', () => {
|
|
32
66
|
|
|
33
67
|
// ── bilibili ──
|
|
34
68
|
it('bilibili hot returns trending videos', async () => {
|
|
35
69
|
const data = await tryBrowserCommand(['bilibili', 'hot', '--limit', '5', '-f', 'json']);
|
|
36
70
|
expectDataOrSkip(data, 'bilibili hot');
|
|
37
|
-
if (data) {
|
|
71
|
+
if (data?.length) {
|
|
38
72
|
expect(data[0]).toHaveProperty('title');
|
|
39
73
|
}
|
|
40
74
|
}, 60_000);
|
|
@@ -53,7 +87,7 @@ describe('browser public-data commands E2E', () => {
|
|
|
53
87
|
it('zhihu hot returns trending questions', async () => {
|
|
54
88
|
const data = await tryBrowserCommand(['zhihu', 'hot', '--limit', '5', '-f', 'json']);
|
|
55
89
|
expectDataOrSkip(data, 'zhihu hot');
|
|
56
|
-
if (data) {
|
|
90
|
+
if (data?.length) {
|
|
57
91
|
expect(data[0]).toHaveProperty('title');
|
|
58
92
|
}
|
|
59
93
|
}, 60_000);
|
|
@@ -68,4 +102,28 @@ describe('browser public-data commands E2E', () => {
|
|
|
68
102
|
const data = await tryBrowserCommand(['v2ex', 'daily', '--limit', '3', '-f', 'json']);
|
|
69
103
|
expectDataOrSkip(data, 'v2ex daily');
|
|
70
104
|
}, 60_000);
|
|
105
|
+
|
|
106
|
+
// ── imdb ──
|
|
107
|
+
it('imdb top returns chart data', async () => {
|
|
108
|
+
const data = await expectImdbDataOrChallengeSkip(['imdb', 'top', '--limit', '3', '-f', 'json'], 'imdb top');
|
|
109
|
+
if (data?.length) {
|
|
110
|
+
expect(data[0]).toHaveProperty('title');
|
|
111
|
+
}
|
|
112
|
+
}, 60_000);
|
|
113
|
+
|
|
114
|
+
it('imdb search returns results', async () => {
|
|
115
|
+
const data = await expectImdbDataOrChallengeSkip(['imdb', 'search', 'inception', '--limit', '3', '-f', 'json'], 'imdb search');
|
|
116
|
+
if (data?.length) {
|
|
117
|
+
expect(data[0]).toHaveProperty('id');
|
|
118
|
+
expect(data[0]).toHaveProperty('title');
|
|
119
|
+
}
|
|
120
|
+
}, 60_000);
|
|
121
|
+
|
|
122
|
+
it('imdb title returns movie details', async () => {
|
|
123
|
+
const data = await expectImdbDataOrChallengeSkip(['imdb', 'title', 'tt1375666', '-f', 'json'], 'imdb title');
|
|
124
|
+
if (data?.length) {
|
|
125
|
+
expect(data[0]).toHaveProperty('field');
|
|
126
|
+
expect(data[0]).toHaveProperty('value');
|
|
127
|
+
}
|
|
128
|
+
}, 60_000);
|
|
71
129
|
});
|
package/tests/e2e/helpers.ts
CHANGED
|
@@ -29,7 +29,8 @@ export async function runCli(
|
|
|
29
29
|
): Promise<CliResult> {
|
|
30
30
|
const timeout = opts.timeout ?? 30_000;
|
|
31
31
|
try {
|
|
32
|
-
const
|
|
32
|
+
const runtime = process.env.OPENCLI_TEST_RUNTIME || 'node';
|
|
33
|
+
const { stdout, stderr } = await exec(runtime, [MAIN, ...args], {
|
|
33
34
|
cwd: ROOT,
|
|
34
35
|
timeout,
|
|
35
36
|
env: {
|