@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/commanderAdapter.ts
CHANGED
|
@@ -16,7 +16,32 @@ import { type CliCommand, fullName, getRegistry } from './registry.js';
|
|
|
16
16
|
import { formatRegistryHelpText } from './serialization.js';
|
|
17
17
|
import { render as renderOutput } from './output.js';
|
|
18
18
|
import { executeCommand } from './execution.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
CliError,
|
|
21
|
+
ERROR_ICONS,
|
|
22
|
+
getErrorMessage,
|
|
23
|
+
BrowserConnectError,
|
|
24
|
+
AuthRequiredError,
|
|
25
|
+
TimeoutError,
|
|
26
|
+
SelectorError,
|
|
27
|
+
EmptyResultError,
|
|
28
|
+
ArgumentError,
|
|
29
|
+
AdapterLoadError,
|
|
30
|
+
CommandExecutionError,
|
|
31
|
+
} from './errors.js';
|
|
32
|
+
import { checkDaemonStatus } from './browser/discover.js';
|
|
33
|
+
|
|
34
|
+
export function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown {
|
|
35
|
+
if (argType !== 'bool') return value;
|
|
36
|
+
if (typeof value === 'boolean') return value;
|
|
37
|
+
if (value == null || value === '') return false;
|
|
38
|
+
|
|
39
|
+
const normalized = String(value).trim().toLowerCase();
|
|
40
|
+
if (normalized === 'true') return true;
|
|
41
|
+
if (normalized === 'false') return false;
|
|
42
|
+
|
|
43
|
+
throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
|
|
44
|
+
}
|
|
20
45
|
|
|
21
46
|
/**
|
|
22
47
|
* Register a single CliCommand as a Commander subcommand.
|
|
@@ -24,7 +49,8 @@ import { CliError, ERROR_ICONS, getErrorMessage } from './errors.js';
|
|
|
24
49
|
export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): void {
|
|
25
50
|
if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) return;
|
|
26
51
|
|
|
27
|
-
const
|
|
52
|
+
const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
|
|
53
|
+
const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
|
|
28
54
|
|
|
29
55
|
// Register positional args first, then named options
|
|
30
56
|
const positionalArgs: typeof cmd.args = [];
|
|
@@ -51,24 +77,29 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
51
77
|
const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts as Record<string, unknown> : {};
|
|
52
78
|
const startTime = Date.now();
|
|
53
79
|
|
|
54
|
-
// ── Collect kwargs ──────────────────────────────────────────────────
|
|
55
|
-
const kwargs: Record<string, unknown> = {};
|
|
56
|
-
for (let i = 0; i < positionalArgs.length; i++) {
|
|
57
|
-
const v = actionArgs[i];
|
|
58
|
-
if (v !== undefined) kwargs[positionalArgs[i].name] = v;
|
|
59
|
-
}
|
|
60
|
-
for (const arg of cmd.args) {
|
|
61
|
-
if (arg.positional) continue;
|
|
62
|
-
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
|
|
63
|
-
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
64
|
-
if (v !== undefined) kwargs[arg.name] = v;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
80
|
// ── Execute + render ────────────────────────────────────────────────
|
|
68
81
|
try {
|
|
82
|
+
// ── Collect kwargs ────────────────────────────────────────────────
|
|
83
|
+
const kwargs: Record<string, unknown> = {};
|
|
84
|
+
for (let i = 0; i < positionalArgs.length; i++) {
|
|
85
|
+
const v = actionArgs[i];
|
|
86
|
+
if (v !== undefined) kwargs[positionalArgs[i].name] = v;
|
|
87
|
+
}
|
|
88
|
+
for (const arg of cmd.args) {
|
|
89
|
+
if (arg.positional) continue;
|
|
90
|
+
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
|
|
91
|
+
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
92
|
+
if (v !== undefined) kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
const verbose = optionsRecord.verbose === true;
|
|
70
96
|
const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
71
97
|
if (verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
98
|
+
if (cmd.deprecated) {
|
|
99
|
+
const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
|
|
100
|
+
const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
|
|
101
|
+
console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
|
|
102
|
+
}
|
|
72
103
|
|
|
73
104
|
const result = await executeCommand(cmd, kwargs, verbose);
|
|
74
105
|
|
|
@@ -85,20 +116,153 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
85
116
|
footerExtra: resolved.footerExtra?.(kwargs),
|
|
86
117
|
});
|
|
87
118
|
} catch (err) {
|
|
88
|
-
|
|
89
|
-
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
90
|
-
console.error(chalk.red(`${icon} ${err.message}`));
|
|
91
|
-
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
92
|
-
} else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
|
|
93
|
-
console.error(chalk.red(err.stack));
|
|
94
|
-
} else {
|
|
95
|
-
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
96
|
-
}
|
|
119
|
+
await renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
97
120
|
process.exitCode = 1;
|
|
98
121
|
}
|
|
99
122
|
});
|
|
100
123
|
}
|
|
101
124
|
|
|
125
|
+
// ── Error rendering ──────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
|
|
128
|
+
|
|
129
|
+
/** Pattern-based classifier for untyped errors thrown by adapters. */
|
|
130
|
+
function classifyGenericError(msg: string): 'auth' | 'http' | 'not-found' | 'other' {
|
|
131
|
+
const m = msg.toLowerCase();
|
|
132
|
+
if (/not logged in|login required|please log in|未登录|请先登录|authentication required|cookie expired/.test(m)) return 'auth';
|
|
133
|
+
// Match "HTTP 404", "status: 500", "status 403", bare "404 Not Found", etc.
|
|
134
|
+
if (/\b(status[: ]+)?[45]\d{2}\b|http[/ ][45]\d{2}/.test(m)) return 'http';
|
|
135
|
+
if (/not found|未找到|could not find|no .+ found/.test(m)) return 'not-found';
|
|
136
|
+
return 'other';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Render a status line for BrowserConnectError based on real-time or kind-derived state. */
|
|
140
|
+
function renderBridgeStatus(running: boolean, extensionConnected: boolean): void {
|
|
141
|
+
const ok = chalk.green('✓');
|
|
142
|
+
const fail = chalk.red('✗');
|
|
143
|
+
console.error(` Daemon ${running ? ok : fail} ${running ? 'running' : 'not running'}`);
|
|
144
|
+
console.error(` Extension ${extensionConnected ? ok : fail} ${extensionConnected ? 'connected' : 'not connected'}`);
|
|
145
|
+
console.error();
|
|
146
|
+
if (!running) {
|
|
147
|
+
console.error(chalk.yellow(' Run the command again — daemon should auto-start.'));
|
|
148
|
+
console.error(chalk.dim(' Still failing? Run: opencli doctor'));
|
|
149
|
+
} else if (!extensionConnected) {
|
|
150
|
+
console.error(chalk.yellow(' Install the Browser Bridge extension to continue:'));
|
|
151
|
+
console.error(chalk.dim(' 1. Download from github.com/jackwener/opencli/releases'));
|
|
152
|
+
console.error(chalk.dim(' 2. chrome://extensions → Enable Developer Mode → Load unpacked'));
|
|
153
|
+
} else {
|
|
154
|
+
console.error(chalk.yellow(' Connection failed despite extension being active.'));
|
|
155
|
+
console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function renderError(err: unknown, cmdName: string, verbose: boolean): Promise<void> {
|
|
160
|
+
// ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
|
|
161
|
+
if (err instanceof BrowserConnectError) {
|
|
162
|
+
console.error(chalk.red('🔌 Browser Bridge not connected'));
|
|
163
|
+
console.error();
|
|
164
|
+
try {
|
|
165
|
+
// 300ms matches execution.ts — localhost responds in <50ms when running.
|
|
166
|
+
const status = await checkDaemonStatus({ timeout: 300 });
|
|
167
|
+
renderBridgeStatus(status.running, status.extensionConnected);
|
|
168
|
+
} catch (_statusErr) {
|
|
169
|
+
// checkDaemonStatus itself failed — derive best-guess state from kind.
|
|
170
|
+
const running = err.kind !== 'daemon-not-running';
|
|
171
|
+
const extensionConnected = err.kind === 'command-failed';
|
|
172
|
+
renderBridgeStatus(running, extensionConnected);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── AuthRequiredError ─────────────────────────────────────────────────
|
|
178
|
+
if (err instanceof AuthRequiredError) {
|
|
179
|
+
console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
|
|
180
|
+
// Respect custom hints set by the adapter; fall back to generic guidance.
|
|
181
|
+
console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── TimeoutError ──────────────────────────────────────────────────────
|
|
186
|
+
if (err instanceof TimeoutError) {
|
|
187
|
+
console.error(chalk.red(`⏱ ${err.message}`));
|
|
188
|
+
console.error(chalk.yellow('→ Try again, or raise the limit:'));
|
|
189
|
+
console.error(chalk.dim(` OPENCLI_BROWSER_COMMAND_TIMEOUT=60 ${cmdName}`));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── SelectorError / EmptyResultError: likely outdated adapter ─────────
|
|
194
|
+
if (err instanceof SelectorError || err instanceof EmptyResultError) {
|
|
195
|
+
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
196
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
197
|
+
console.error(chalk.yellow('→ The page structure may have changed — this adapter may be outdated.'));
|
|
198
|
+
console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
|
|
199
|
+
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── ArgumentError ─────────────────────────────────────────────────────
|
|
204
|
+
if (err instanceof ArgumentError) {
|
|
205
|
+
console.error(chalk.red(`❌ ${err.message}`));
|
|
206
|
+
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── AdapterLoadError ──────────────────────────────────────────────────
|
|
211
|
+
if (err instanceof AdapterLoadError) {
|
|
212
|
+
console.error(chalk.red(`📦 ${err.message}`));
|
|
213
|
+
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── CommandExecutionError ─────────────────────────────────────────────
|
|
218
|
+
if (err instanceof CommandExecutionError) {
|
|
219
|
+
console.error(chalk.red(`💥 ${err.message}`));
|
|
220
|
+
if (err.hint) {
|
|
221
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
222
|
+
} else {
|
|
223
|
+
console.error(chalk.dim(` Add --verbose for details, or report: ${ISSUES_URL}`));
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Other typed CliError (fallback for future codes) ──────────────────
|
|
229
|
+
if (err instanceof CliError) {
|
|
230
|
+
const icon = ERROR_ICONS[err.code] ?? '⚠️';
|
|
231
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
232
|
+
if (err.hint) console.error(chalk.yellow(`→ ${err.hint}`));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Generic Error from adapters: classify by message pattern ──────────
|
|
237
|
+
const msg = getErrorMessage(err);
|
|
238
|
+
const kind = classifyGenericError(msg);
|
|
239
|
+
|
|
240
|
+
if (kind === 'auth') {
|
|
241
|
+
console.error(chalk.red(`🔒 ${msg}`));
|
|
242
|
+
console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (kind === 'http') {
|
|
246
|
+
console.error(chalk.red(`🌐 ${msg}`));
|
|
247
|
+
console.error(chalk.yellow('→ Check your login status, or the site may be temporarily unavailable.'));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (kind === 'not-found') {
|
|
251
|
+
console.error(chalk.red(`📭 ${msg}`));
|
|
252
|
+
console.error(chalk.yellow('→ The resource was not found. The adapter or page structure may have changed.'));
|
|
253
|
+
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Unknown error: show stack in verbose mode ─────────────────────────
|
|
258
|
+
if (verbose && err instanceof Error && err.stack) {
|
|
259
|
+
console.error(chalk.red(err.stack));
|
|
260
|
+
} else {
|
|
261
|
+
console.error(chalk.red(`💥 Unexpected error: ${msg}`));
|
|
262
|
+
console.error(chalk.dim(` Run with --verbose for details, or report: ${ISSUES_URL}`));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
102
266
|
/**
|
|
103
267
|
* Register all commands from the registry onto a Commander program.
|
|
104
268
|
*/
|
package/src/daemon.ts
CHANGED
|
@@ -29,6 +29,7 @@ const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
|
29
29
|
// ─── State ───────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
31
|
let extensionWs: WebSocket | null = null;
|
|
32
|
+
let extensionVersion: string | null = null;
|
|
32
33
|
const pending = new Map<string, {
|
|
33
34
|
resolve: (data: unknown) => void;
|
|
34
35
|
reject: (error: Error) => void;
|
|
@@ -117,6 +118,7 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
117
118
|
jsonResponse(res, 200, {
|
|
118
119
|
ok: true,
|
|
119
120
|
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
121
|
+
extensionVersion,
|
|
120
122
|
pending: pending.size,
|
|
121
123
|
});
|
|
122
124
|
return;
|
|
@@ -222,6 +224,12 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
222
224
|
try {
|
|
223
225
|
const msg = JSON.parse(data.toString());
|
|
224
226
|
|
|
227
|
+
// Handle hello message from extension (version handshake)
|
|
228
|
+
if (msg.type === 'hello') {
|
|
229
|
+
extensionVersion = typeof msg.version === 'string' ? msg.version : null;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
225
233
|
// Handle log messages from extension
|
|
226
234
|
if (msg.type === 'log') {
|
|
227
235
|
const prefix = msg.level === 'error' ? '❌' : msg.level === 'warn' ? '⚠️' : '📋';
|
|
@@ -247,6 +255,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
247
255
|
clearInterval(heartbeatInterval);
|
|
248
256
|
if (extensionWs === ws) {
|
|
249
257
|
extensionWs = null;
|
|
258
|
+
extensionVersion = null;
|
|
250
259
|
// Reject all pending requests since the extension is gone
|
|
251
260
|
for (const [id, p] of pending) {
|
|
252
261
|
clearTimeout(p.timer);
|
|
@@ -258,7 +267,16 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
258
267
|
|
|
259
268
|
ws.on('error', () => {
|
|
260
269
|
clearInterval(heartbeatInterval);
|
|
261
|
-
if (extensionWs === ws)
|
|
270
|
+
if (extensionWs === ws) {
|
|
271
|
+
extensionWs = null;
|
|
272
|
+
extensionVersion = null;
|
|
273
|
+
// Reject pending requests in case 'close' does not follow this 'error'
|
|
274
|
+
for (const [, p] of pending) {
|
|
275
|
+
clearTimeout(p.timer);
|
|
276
|
+
p.reject(new Error('Extension disconnected'));
|
|
277
|
+
}
|
|
278
|
+
pending.clear();
|
|
279
|
+
}
|
|
262
280
|
});
|
|
263
281
|
});
|
|
264
282
|
|
package/src/discovery.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
|
|
|
23
23
|
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
24
24
|
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
25
25
|
|
|
26
|
-
import type
|
|
26
|
+
import { type YamlCliDefinition, parseYamlArgs } from './yaml-schema.js';
|
|
27
27
|
|
|
28
28
|
function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
|
|
29
29
|
if (!rawStrategy) return fallback;
|
|
@@ -77,6 +77,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
77
77
|
pipeline: entry.pipeline,
|
|
78
78
|
timeoutSeconds: entry.timeout,
|
|
79
79
|
source: `manifest:${entry.site}/${entry.name}`,
|
|
80
|
+
deprecated: entry.deprecated,
|
|
81
|
+
replacedBy: entry.replacedBy,
|
|
80
82
|
navigateBefore: entry.navigateBefore,
|
|
81
83
|
};
|
|
82
84
|
registerCommand(cmd);
|
|
@@ -96,6 +98,8 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
|
|
|
96
98
|
columns: entry.columns,
|
|
97
99
|
timeoutSeconds: entry.timeout,
|
|
98
100
|
source: modulePath,
|
|
101
|
+
deprecated: entry.deprecated,
|
|
102
|
+
replacedBy: entry.replacedBy,
|
|
99
103
|
navigateBefore: entry.navigateBefore,
|
|
100
104
|
_lazy: true,
|
|
101
105
|
_modulePath: modulePath,
|
|
@@ -123,24 +127,20 @@ async function discoverClisFromFs(dir: string): Promise<void> {
|
|
|
123
127
|
const site = entry.name;
|
|
124
128
|
const siteDir = path.join(dir, site);
|
|
125
129
|
const files = await fs.promises.readdir(siteDir);
|
|
126
|
-
|
|
127
|
-
for (const file of files) {
|
|
130
|
+
await Promise.all(files.map(async (file) => {
|
|
128
131
|
const filePath = path.join(siteDir, file);
|
|
129
132
|
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
130
|
-
|
|
133
|
+
await registerYamlCli(filePath, site);
|
|
131
134
|
} else if (
|
|
132
135
|
(file.endsWith('.js') && !file.endsWith('.d.js')) ||
|
|
133
136
|
(file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))
|
|
134
137
|
) {
|
|
135
|
-
if (!(await isCliModule(filePath)))
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
})
|
|
140
|
-
);
|
|
138
|
+
if (!(await isCliModule(filePath))) return;
|
|
139
|
+
await import(pathToFileURL(filePath).href).catch((err) => {
|
|
140
|
+
log.warn(`Failed to load module ${filePath}: ${getErrorMessage(err)}`);
|
|
141
|
+
});
|
|
141
142
|
}
|
|
142
|
-
}
|
|
143
|
-
await Promise.all(filePromises);
|
|
143
|
+
}));
|
|
144
144
|
});
|
|
145
145
|
await Promise.all(sitePromises);
|
|
146
146
|
}
|
|
@@ -158,20 +158,7 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
|
|
|
158
158
|
const strategy = parseStrategy(strategyStr);
|
|
159
159
|
const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
|
|
160
160
|
|
|
161
|
-
const args
|
|
162
|
-
if (cliDef.args && typeof cliDef.args === 'object') {
|
|
163
|
-
for (const [argName, argDef] of Object.entries(cliDef.args)) {
|
|
164
|
-
args.push({
|
|
165
|
-
name: argName,
|
|
166
|
-
type: argDef?.type ?? 'str',
|
|
167
|
-
default: argDef?.default,
|
|
168
|
-
required: argDef?.required ?? false,
|
|
169
|
-
positional: argDef?.positional ?? false,
|
|
170
|
-
help: argDef?.description ?? argDef?.help ?? '',
|
|
171
|
-
choices: argDef?.choices,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
161
|
+
const args = parseYamlArgs(cliDef.args);
|
|
175
162
|
|
|
176
163
|
const cmd: CliCommand = {
|
|
177
164
|
site,
|
|
@@ -185,6 +172,8 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
|
|
|
185
172
|
pipeline: cliDef.pipeline,
|
|
186
173
|
timeoutSeconds: cliDef.timeout,
|
|
187
174
|
source: filePath,
|
|
175
|
+
deprecated: (cliDef as Record<string, unknown>).deprecated as boolean | string | undefined,
|
|
176
|
+
replacedBy: (cliDef as Record<string, unknown>).replacedBy as string | undefined,
|
|
188
177
|
navigateBefore: cliDef.navigateBefore,
|
|
189
178
|
};
|
|
190
179
|
|
|
@@ -202,10 +191,11 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
|
|
|
202
191
|
export async function discoverPlugins(): Promise<void> {
|
|
203
192
|
try { await fs.promises.access(PLUGINS_DIR); } catch { return; }
|
|
204
193
|
const entries = await fs.promises.readdir(PLUGINS_DIR, { withFileTypes: true });
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
await Promise.all(entries.map(async (entry) => {
|
|
195
|
+
const pluginDir = path.join(PLUGINS_DIR, entry.name);
|
|
196
|
+
if (!(await isDiscoverablePluginDir(entry, pluginDir))) return;
|
|
197
|
+
await discoverPluginDir(pluginDir, entry.name);
|
|
198
|
+
}));
|
|
209
199
|
}
|
|
210
200
|
|
|
211
201
|
/**
|
|
@@ -215,33 +205,29 @@ export async function discoverPlugins(): Promise<void> {
|
|
|
215
205
|
async function discoverPluginDir(dir: string, site: string): Promise<void> {
|
|
216
206
|
const files = await fs.promises.readdir(dir);
|
|
217
207
|
const fileSet = new Set(files);
|
|
218
|
-
|
|
219
|
-
for (const file of files) {
|
|
208
|
+
await Promise.all(files.map(async (file) => {
|
|
220
209
|
const filePath = path.join(dir, file);
|
|
221
210
|
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
222
|
-
|
|
211
|
+
await registerYamlCli(filePath, site);
|
|
223
212
|
} else if (file.endsWith('.js') && !file.endsWith('.d.js')) {
|
|
224
|
-
if (!(await isCliModule(filePath)))
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
})
|
|
229
|
-
);
|
|
213
|
+
if (!(await isCliModule(filePath))) return;
|
|
214
|
+
await import(pathToFileURL(filePath).href).catch((err) => {
|
|
215
|
+
log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
|
|
216
|
+
});
|
|
230
217
|
} else if (
|
|
231
218
|
file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts')
|
|
232
219
|
) {
|
|
233
|
-
// Skip .ts if a compiled .js sibling exists (production mode can't load .ts)
|
|
234
220
|
const jsFile = file.replace(/\.ts$/, '.js');
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
221
|
+
// Prefer compiled .js — skip the .ts source file
|
|
222
|
+
if (fileSet.has(jsFile)) return;
|
|
223
|
+
// No compiled .js found — cannot import raw .ts in production Node.js.
|
|
224
|
+
// This typically means esbuild transpilation failed during plugin install.
|
|
225
|
+
log.warn(
|
|
226
|
+
`Plugin ${site}/${file}: no compiled .js found. ` +
|
|
227
|
+
`Run "opencli plugin update ${site}" to re-transpile, or install esbuild.`
|
|
241
228
|
);
|
|
242
229
|
}
|
|
243
|
-
}
|
|
244
|
-
await Promise.all(promises);
|
|
230
|
+
}));
|
|
245
231
|
}
|
|
246
232
|
|
|
247
233
|
async function isCliModule(filePath: string): Promise<boolean> {
|
|
@@ -253,3 +239,18 @@ async function isCliModule(filePath: string): Promise<boolean> {
|
|
|
253
239
|
return false;
|
|
254
240
|
}
|
|
255
241
|
}
|
|
242
|
+
|
|
243
|
+
async function isDiscoverablePluginDir(entry: fs.Dirent, pluginDir: string): Promise<boolean> {
|
|
244
|
+
if (entry.isDirectory()) return true;
|
|
245
|
+
if (!entry.isSymbolicLink()) return false;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
return (await fs.promises.stat(pluginDir)).isDirectory();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
251
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
252
|
+
log.warn(`Failed to inspect plugin link ${pluginDir}: ${getErrorMessage(err)}`);
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
package/src/doctor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* opencli doctor — diagnose
|
|
2
|
+
* opencli doctor — diagnose browser connectivity.
|
|
3
3
|
*
|
|
4
4
|
* Simplified for the daemon-based architecture. No more token management,
|
|
5
5
|
* MCP path discovery, or config file scanning.
|
|
@@ -11,9 +11,9 @@ import { checkDaemonStatus } from './browser/discover.js';
|
|
|
11
11
|
import { BrowserBridge } from './browser/index.js';
|
|
12
12
|
import { listSessions } from './browser/daemon-client.js';
|
|
13
13
|
import { getErrorMessage } from './errors.js';
|
|
14
|
+
import { getRuntimeLabel } from './runtime-detect.js';
|
|
14
15
|
|
|
15
16
|
export type DoctorOptions = {
|
|
16
|
-
fix?: boolean;
|
|
17
17
|
yes?: boolean;
|
|
18
18
|
live?: boolean;
|
|
19
19
|
sessions?: boolean;
|
|
@@ -30,6 +30,7 @@ export type DoctorReport = {
|
|
|
30
30
|
cliVersion?: string;
|
|
31
31
|
daemonRunning: boolean;
|
|
32
32
|
extensionConnected: boolean;
|
|
33
|
+
extensionVersion?: string;
|
|
33
34
|
connectivity?: ConnectivityResult;
|
|
34
35
|
sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
|
|
35
36
|
issues: string[];
|
|
@@ -85,7 +86,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
85
86
|
issues.push(
|
|
86
87
|
'Daemon is running but the Chrome extension is not connected.\n' +
|
|
87
88
|
'Please install the opencli Browser Bridge extension:\n' +
|
|
88
|
-
' 1. Download from
|
|
89
|
+
' 1. Download from https://github.com/jackwener/opencli/releases\n' +
|
|
89
90
|
' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
|
|
90
91
|
' 3. Click "Load unpacked" → select the extension folder',
|
|
91
92
|
);
|
|
@@ -94,10 +95,18 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
94
95
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
if (status.extensionVersion && opts.cliVersion && status.extensionVersion !== opts.cliVersion) {
|
|
99
|
+
issues.push(
|
|
100
|
+
`Extension version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
|
|
101
|
+
' Download the latest extension from: https://github.com/jackwener/opencli/releases',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
return {
|
|
98
106
|
cliVersion: opts.cliVersion,
|
|
99
107
|
daemonRunning: status.running,
|
|
100
108
|
extensionConnected: status.extensionConnected,
|
|
109
|
+
extensionVersion: status.extensionVersion,
|
|
101
110
|
connectivity,
|
|
102
111
|
sessions,
|
|
103
112
|
issues,
|
|
@@ -105,7 +114,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
108
|
-
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
|
117
|
+
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
|
|
109
118
|
|
|
110
119
|
// Daemon status
|
|
111
120
|
const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
|
|
@@ -113,7 +122,8 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
113
122
|
|
|
114
123
|
// Extension status
|
|
115
124
|
const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
|
|
116
|
-
|
|
125
|
+
const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
|
|
126
|
+
lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
|
|
117
127
|
|
|
118
128
|
// Connectivity
|
|
119
129
|
if (report.connectivity) {
|
|
@@ -6,12 +6,17 @@ import { afterEach, describe, expect, it } from 'vitest';
|
|
|
6
6
|
import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
|
|
7
7
|
|
|
8
8
|
const servers: http.Server[] = [];
|
|
9
|
+
const tempDirs: string[] = [];
|
|
9
10
|
|
|
10
11
|
afterEach(async () => {
|
|
11
12
|
await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
|
|
12
13
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
13
14
|
})));
|
|
14
15
|
servers.length = 0;
|
|
16
|
+
for (const dir of tempDirs) {
|
|
17
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
18
|
+
}
|
|
19
|
+
tempDirs.length = 0;
|
|
15
20
|
});
|
|
16
21
|
|
|
17
22
|
async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> {
|
|
@@ -25,7 +30,9 @@ async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'
|
|
|
25
30
|
return `http://${hostname}:${address.port}`;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
// Windows Defender can briefly lock newly-written .tmp files, causing EPERM.
|
|
34
|
+
// Retry once to handle this flakiness.
|
|
35
|
+
describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
|
|
29
36
|
it('resolves relative redirects against the original URL', () => {
|
|
30
37
|
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
31
38
|
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
@@ -45,7 +52,8 @@ describe('download helpers', () => {
|
|
|
45
52
|
res.end();
|
|
46
53
|
});
|
|
47
54
|
|
|
48
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
55
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
56
|
+
tempDirs.push(tempDir);
|
|
49
57
|
const destPath = path.join(tempDir, 'file.txt');
|
|
50
58
|
const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
|
|
51
59
|
|
|
@@ -71,7 +79,8 @@ describe('download helpers', () => {
|
|
|
71
79
|
res.end();
|
|
72
80
|
});
|
|
73
81
|
|
|
74
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
82
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
83
|
+
tempDirs.push(tempDir);
|
|
75
84
|
const destPath = path.join(tempDir, 'redirect.txt');
|
|
76
85
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
|
|
77
86
|
|
|
@@ -94,7 +103,8 @@ describe('download helpers', () => {
|
|
|
94
103
|
res.end();
|
|
95
104
|
});
|
|
96
105
|
|
|
97
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
106
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
107
|
+
tempDirs.push(tempDir);
|
|
98
108
|
const destPath = path.join(tempDir, 'redirect-header.txt');
|
|
99
109
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, {
|
|
100
110
|
headers: { Cookie: 'sid=header-cookie' },
|