@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/dist/download/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import * as path from 'node:path';
|
|
|
7
7
|
import * as https from 'node:https';
|
|
8
8
|
import * as http from 'node:http';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
|
+
import { Transform } from 'node:stream';
|
|
11
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
12
|
import { URL } from 'node:url';
|
|
11
13
|
import { isBinaryInstalled } from '../external.js';
|
|
12
14
|
import { getErrorMessage } from '../errors.js';
|
|
@@ -68,65 +70,75 @@ export async function httpDownload(url, destPath, options = {}, redirectCount =
|
|
|
68
70
|
if (cookies) {
|
|
69
71
|
requestHeaders['Cookie'] = cookies;
|
|
70
72
|
}
|
|
71
|
-
// Ensure directory exists
|
|
72
|
-
const dir = path.dirname(destPath);
|
|
73
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
74
73
|
const tempPath = `${destPath}.tmp`;
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
79
|
-
file.close();
|
|
80
|
-
if (fs.existsSync(tempPath))
|
|
81
|
-
fs.unlinkSync(tempPath);
|
|
82
|
-
if (redirectCount >= maxRedirects) {
|
|
83
|
-
resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
87
|
-
const originalHost = new URL(url).hostname;
|
|
88
|
-
const redirectHost = new URL(redirectUrl).hostname;
|
|
89
|
-
// Do not forward cookies when a redirect crosses host boundaries.
|
|
90
|
-
const redirectOptions = originalHost === redirectHost
|
|
91
|
-
? options
|
|
92
|
-
: { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
|
|
93
|
-
httpDownload(redirectUrl, destPath, redirectOptions, redirectCount + 1).then(resolve);
|
|
74
|
+
let settled = false;
|
|
75
|
+
const finish = (result) => {
|
|
76
|
+
if (settled)
|
|
94
77
|
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
resolve(result);
|
|
80
|
+
};
|
|
81
|
+
const cleanupTempFile = async () => {
|
|
82
|
+
try {
|
|
83
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
95
84
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (fs.existsSync(tempPath))
|
|
99
|
-
fs.unlinkSync(tempPath);
|
|
100
|
-
resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
101
|
-
return;
|
|
85
|
+
catch {
|
|
86
|
+
// Ignore cleanup errors so the original failure is preserved.
|
|
102
87
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
88
|
+
};
|
|
89
|
+
const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
|
|
90
|
+
void (async () => {
|
|
91
|
+
// Handle redirects before creating any file handles.
|
|
92
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
93
|
+
response.resume();
|
|
94
|
+
if (redirectCount >= maxRedirects) {
|
|
95
|
+
finish({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
99
|
+
const originalHost = new URL(url).hostname;
|
|
100
|
+
const redirectHost = new URL(redirectUrl).hostname;
|
|
101
|
+
const redirectOptions = originalHost === redirectHost
|
|
102
|
+
? options
|
|
103
|
+
: { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
|
|
104
|
+
finish(await httpDownload(redirectUrl, destPath, redirectOptions, redirectCount + 1));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (response.statusCode !== 200) {
|
|
108
|
+
response.resume();
|
|
109
|
+
finish({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
|
113
|
+
let received = 0;
|
|
114
|
+
const progressStream = new Transform({
|
|
115
|
+
transform(chunk, _encoding, callback) {
|
|
116
|
+
received += chunk.length;
|
|
117
|
+
if (onProgress)
|
|
118
|
+
onProgress(received, totalSize);
|
|
119
|
+
callback(null, chunk);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
124
|
+
await pipeline(response, progressStream, fs.createWriteStream(tempPath));
|
|
125
|
+
await fs.promises.rename(tempPath, destPath);
|
|
126
|
+
finish({ success: true, size: received });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
await cleanupTempFile();
|
|
130
|
+
finish({ success: false, size: 0, error: getErrorMessage(err) });
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
117
133
|
});
|
|
118
134
|
request.on('error', (err) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
void (async () => {
|
|
136
|
+
await cleanupTempFile();
|
|
137
|
+
finish({ success: false, size: 0, error: err.message });
|
|
138
|
+
})();
|
|
123
139
|
});
|
|
124
140
|
request.on('timeout', () => {
|
|
125
|
-
request.destroy();
|
|
126
|
-
file.close();
|
|
127
|
-
if (fs.existsSync(tempPath))
|
|
128
|
-
fs.unlinkSync(tempPath);
|
|
129
|
-
resolve({ success: false, size: 0, error: 'Timeout' });
|
|
141
|
+
request.destroy(new Error('Timeout'));
|
|
130
142
|
});
|
|
131
143
|
});
|
|
132
144
|
}
|
|
@@ -5,11 +5,19 @@ import * as path from 'node:path';
|
|
|
5
5
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
6
|
import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
|
|
7
7
|
const servers = [];
|
|
8
|
+
const tempDirs = [];
|
|
8
9
|
afterEach(async () => {
|
|
9
10
|
await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
|
|
10
11
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
11
12
|
})));
|
|
12
13
|
servers.length = 0;
|
|
14
|
+
for (const dir of tempDirs) {
|
|
15
|
+
try {
|
|
16
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
catch { /* ignore */ }
|
|
19
|
+
}
|
|
20
|
+
tempDirs.length = 0;
|
|
13
21
|
});
|
|
14
22
|
async function startServer(handler, hostname = '127.0.0.1') {
|
|
15
23
|
const server = http.createServer(handler);
|
|
@@ -21,7 +29,9 @@ async function startServer(handler, hostname = '127.0.0.1') {
|
|
|
21
29
|
}
|
|
22
30
|
return `http://${hostname}:${address.port}`;
|
|
23
31
|
}
|
|
24
|
-
|
|
32
|
+
// Windows Defender can briefly lock newly-written .tmp files, causing EPERM.
|
|
33
|
+
// Retry once to handle this flakiness.
|
|
34
|
+
describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
|
|
25
35
|
it('resolves relative redirects against the original URL', () => {
|
|
26
36
|
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
27
37
|
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
@@ -38,7 +48,8 @@ describe('download helpers', () => {
|
|
|
38
48
|
res.setHeader('Location', '/loop');
|
|
39
49
|
res.end();
|
|
40
50
|
});
|
|
41
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
51
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
52
|
+
tempDirs.push(tempDir);
|
|
42
53
|
const destPath = path.join(tempDir, 'file.txt');
|
|
43
54
|
const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
|
|
44
55
|
expect(result).toEqual({
|
|
@@ -60,7 +71,8 @@ describe('download helpers', () => {
|
|
|
60
71
|
res.setHeader('Location', targetUrl);
|
|
61
72
|
res.end();
|
|
62
73
|
});
|
|
63
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
74
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
75
|
+
tempDirs.push(tempDir);
|
|
64
76
|
const destPath = path.join(tempDir, 'redirect.txt');
|
|
65
77
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
|
|
66
78
|
expect(result).toEqual({ success: true, size: 2 });
|
|
@@ -79,7 +91,8 @@ describe('download helpers', () => {
|
|
|
79
91
|
res.setHeader('Location', targetUrl);
|
|
80
92
|
res.end();
|
|
81
93
|
});
|
|
82
|
-
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-
|
|
94
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
|
|
95
|
+
tempDirs.push(tempDir);
|
|
83
96
|
const destPath = path.join(tempDir, 'redirect-header.txt');
|
|
84
97
|
const result = await httpDownload(`${redirectUrl}/start`, destPath, {
|
|
85
98
|
headers: { Cookie: 'sid=header-cookie' },
|
package/dist/engine.test.js
CHANGED
|
@@ -75,11 +75,26 @@ cli({
|
|
|
75
75
|
describe('discoverPlugins', () => {
|
|
76
76
|
const testPluginDir = path.join(PLUGINS_DIR, '__test-plugin__');
|
|
77
77
|
const yamlPath = path.join(testPluginDir, 'greeting.yaml');
|
|
78
|
+
const symlinkTargetDir = path.join(os.tmpdir(), '__test-plugin-symlink-target__');
|
|
79
|
+
const symlinkPluginDir = path.join(PLUGINS_DIR, '__test-plugin-symlink__');
|
|
80
|
+
const brokenSymlinkDir = path.join(PLUGINS_DIR, '__test-plugin-broken__');
|
|
78
81
|
afterEach(async () => {
|
|
79
82
|
try {
|
|
80
83
|
await fs.promises.rm(testPluginDir, { recursive: true });
|
|
81
84
|
}
|
|
82
85
|
catch { }
|
|
86
|
+
try {
|
|
87
|
+
await fs.promises.rm(symlinkPluginDir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
try {
|
|
91
|
+
await fs.promises.rm(symlinkTargetDir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
try {
|
|
95
|
+
await fs.promises.rm(brokenSymlinkDir, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
83
98
|
});
|
|
84
99
|
it('discovers YAML plugins from ~/.opencli/plugins/', async () => {
|
|
85
100
|
// Create a simple YAML adapter in the plugins directory
|
|
@@ -108,6 +123,33 @@ columns: [message]
|
|
|
108
123
|
// discoverPlugins should not throw if ~/.opencli/plugins/ does not exist
|
|
109
124
|
await expect(discoverPlugins()).resolves.not.toThrow();
|
|
110
125
|
});
|
|
126
|
+
it('discovers YAML plugins from symlinked plugin directories', async () => {
|
|
127
|
+
await fs.promises.mkdir(PLUGINS_DIR, { recursive: true });
|
|
128
|
+
await fs.promises.mkdir(symlinkTargetDir, { recursive: true });
|
|
129
|
+
await fs.promises.writeFile(path.join(symlinkTargetDir, 'hello.yaml'), `
|
|
130
|
+
site: __test-plugin-symlink__
|
|
131
|
+
name: hello
|
|
132
|
+
description: Test plugin greeting via symlink
|
|
133
|
+
strategy: public
|
|
134
|
+
browser: false
|
|
135
|
+
|
|
136
|
+
pipeline:
|
|
137
|
+
- evaluate: "() => [{ message: 'hello from symlink plugin' }]"
|
|
138
|
+
|
|
139
|
+
columns: [message]
|
|
140
|
+
`);
|
|
141
|
+
await fs.promises.symlink(symlinkTargetDir, symlinkPluginDir, 'dir');
|
|
142
|
+
await discoverPlugins();
|
|
143
|
+
const cmd = getRegistry().get('__test-plugin-symlink__/hello');
|
|
144
|
+
expect(cmd).toBeDefined();
|
|
145
|
+
expect(cmd.description).toBe('Test plugin greeting via symlink');
|
|
146
|
+
});
|
|
147
|
+
it('skips broken plugin symlinks without throwing', async () => {
|
|
148
|
+
await fs.promises.mkdir(PLUGINS_DIR, { recursive: true });
|
|
149
|
+
await fs.promises.symlink(path.join(os.tmpdir(), '__missing-plugin-target__'), brokenSymlinkDir, 'dir');
|
|
150
|
+
await expect(discoverPlugins()).resolves.not.toThrow();
|
|
151
|
+
expect(getRegistry().get('__test-plugin-broken__/hello')).toBeUndefined();
|
|
152
|
+
});
|
|
111
153
|
});
|
|
112
154
|
describe('executeCommand', () => {
|
|
113
155
|
beforeEach(() => {
|
package/dist/errors.d.ts
CHANGED
|
@@ -12,8 +12,10 @@ export declare class CliError extends Error {
|
|
|
12
12
|
readonly hint?: string;
|
|
13
13
|
constructor(code: string, message: string, hint?: string);
|
|
14
14
|
}
|
|
15
|
+
export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
|
|
15
16
|
export declare class BrowserConnectError extends CliError {
|
|
16
|
-
|
|
17
|
+
readonly kind: BrowserConnectKind;
|
|
18
|
+
constructor(message: string, hint?: string, kind?: BrowserConnectKind);
|
|
17
19
|
}
|
|
18
20
|
export declare class AdapterLoadError extends CliError {
|
|
19
21
|
constructor(message: string, hint?: string);
|
|
@@ -29,7 +31,7 @@ export declare class AuthRequiredError extends CliError {
|
|
|
29
31
|
constructor(domain: string, message?: string);
|
|
30
32
|
}
|
|
31
33
|
export declare class TimeoutError extends CliError {
|
|
32
|
-
constructor(label: string, seconds: number);
|
|
34
|
+
constructor(label: string, seconds: number, hint?: string);
|
|
33
35
|
}
|
|
34
36
|
export declare class ArgumentError extends CliError {
|
|
35
37
|
constructor(message: string, hint?: string);
|
package/dist/errors.js
CHANGED
|
@@ -12,74 +12,50 @@ export class CliError extends Error {
|
|
|
12
12
|
hint;
|
|
13
13
|
constructor(code, message, hint) {
|
|
14
14
|
super(message);
|
|
15
|
-
this.name =
|
|
15
|
+
this.name = new.target.name;
|
|
16
16
|
this.code = code;
|
|
17
17
|
this.hint = hint;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
// ── Browser / Connection ────────────────────────────────────────────────────
|
|
21
20
|
export class BrowserConnectError extends CliError {
|
|
22
|
-
|
|
21
|
+
kind;
|
|
22
|
+
constructor(message, hint, kind = 'unknown') {
|
|
23
23
|
super('BROWSER_CONNECT', message, hint);
|
|
24
|
-
this.
|
|
24
|
+
this.kind = kind;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
// ── Adapter loading ─────────────────────────────────────────────────────────
|
|
28
27
|
export class AdapterLoadError extends CliError {
|
|
29
|
-
constructor(message, hint) {
|
|
30
|
-
super('ADAPTER_LOAD', message, hint);
|
|
31
|
-
this.name = 'AdapterLoadError';
|
|
32
|
-
}
|
|
28
|
+
constructor(message, hint) { super('ADAPTER_LOAD', message, hint); }
|
|
33
29
|
}
|
|
34
|
-
// ── Command execution ───────────────────────────────────────────────────────
|
|
35
30
|
export class CommandExecutionError extends CliError {
|
|
36
|
-
constructor(message, hint) {
|
|
37
|
-
super('COMMAND_EXEC', message, hint);
|
|
38
|
-
this.name = 'CommandExecutionError';
|
|
39
|
-
}
|
|
31
|
+
constructor(message, hint) { super('COMMAND_EXEC', message, hint); }
|
|
40
32
|
}
|
|
41
|
-
// ── Configuration ───────────────────────────────────────────────────────────
|
|
42
33
|
export class ConfigError extends CliError {
|
|
43
|
-
constructor(message, hint) {
|
|
44
|
-
super('CONFIG', message, hint);
|
|
45
|
-
this.name = 'ConfigError';
|
|
46
|
-
}
|
|
34
|
+
constructor(message, hint) { super('CONFIG', message, hint); }
|
|
47
35
|
}
|
|
48
|
-
// ── Authentication / Login ──────────────────────────────────────────────────
|
|
49
36
|
export class AuthRequiredError extends CliError {
|
|
50
37
|
domain;
|
|
51
38
|
constructor(domain, message) {
|
|
52
39
|
super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`);
|
|
53
|
-
this.name = 'AuthRequiredError';
|
|
54
40
|
this.domain = domain;
|
|
55
41
|
}
|
|
56
42
|
}
|
|
57
|
-
// ── Timeout ─────────────────────────────────────────────────────────────────
|
|
58
43
|
export class TimeoutError extends CliError {
|
|
59
|
-
constructor(label, seconds) {
|
|
60
|
-
super('TIMEOUT', `${label} timed out after ${seconds}s`, 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var');
|
|
61
|
-
this.name = 'TimeoutError';
|
|
44
|
+
constructor(label, seconds, hint) {
|
|
45
|
+
super('TIMEOUT', `${label} timed out after ${seconds}s`, hint ?? 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var');
|
|
62
46
|
}
|
|
63
47
|
}
|
|
64
|
-
// ── Argument validation ─────────────────────────────────────────────────────
|
|
65
48
|
export class ArgumentError extends CliError {
|
|
66
|
-
constructor(message, hint) {
|
|
67
|
-
super('ARGUMENT', message, hint);
|
|
68
|
-
this.name = 'ArgumentError';
|
|
69
|
-
}
|
|
49
|
+
constructor(message, hint) { super('ARGUMENT', message, hint); }
|
|
70
50
|
}
|
|
71
|
-
// ── Empty result ────────────────────────────────────────────────────────────
|
|
72
51
|
export class EmptyResultError extends CliError {
|
|
73
52
|
constructor(command, hint) {
|
|
74
53
|
super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in');
|
|
75
|
-
this.name = 'EmptyResultError';
|
|
76
54
|
}
|
|
77
55
|
}
|
|
78
|
-
// ── Selector / DOM ──────────────────────────────────────────────────────────
|
|
79
56
|
export class SelectorError extends CliError {
|
|
80
57
|
constructor(selector, hint) {
|
|
81
58
|
super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.');
|
|
82
|
-
this.name = 'SelectorError';
|
|
83
59
|
}
|
|
84
60
|
}
|
|
85
61
|
// ── Utilities ───────────────────────────────────────────────────────────
|
|
@@ -95,4 +71,11 @@ export const ERROR_ICONS = {
|
|
|
95
71
|
ARGUMENT: '❌',
|
|
96
72
|
EMPTY_RESULT: '📭',
|
|
97
73
|
SELECTOR: '🔍',
|
|
74
|
+
COMMAND_EXEC: '💥',
|
|
75
|
+
ADAPTER_LOAD: '📦',
|
|
76
|
+
NETWORK: '🌐',
|
|
77
|
+
API_ERROR: '🚫',
|
|
78
|
+
RATE_LIMITED: '⏳',
|
|
79
|
+
PAGE_CHANGED: '🔄',
|
|
80
|
+
CONFIG: '⚙️ ',
|
|
98
81
|
};
|
package/dist/execution.d.ts
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
* 5. Lazy-loading of TS modules from manifest
|
|
10
10
|
* 6. Lifecycle hooks (onBeforeExecute / onAfterExecute)
|
|
11
11
|
*/
|
|
12
|
-
import { type CliCommand, type Arg } from './registry.js';
|
|
13
|
-
type CommandArgs = Record<string, unknown>;
|
|
12
|
+
import { type CliCommand, type Arg, type CommandArgs } from './registry.js';
|
|
14
13
|
export declare function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): CommandArgs;
|
|
15
14
|
export declare function executeCommand(cmd: CliCommand, rawKwargs: CommandArgs, debug?: boolean): Promise<unknown>;
|
|
16
|
-
export {};
|
package/dist/execution.js
CHANGED
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
import { Strategy, getRegistry, fullName } from './registry.js';
|
|
13
13
|
import { pathToFileURL } from 'node:url';
|
|
14
14
|
import { executePipeline } from './pipeline/index.js';
|
|
15
|
-
import { AdapterLoadError, ArgumentError, CommandExecutionError, getErrorMessage } from './errors.js';
|
|
15
|
+
import { AdapterLoadError, ArgumentError, BrowserConnectError, CommandExecutionError, getErrorMessage } from './errors.js';
|
|
16
16
|
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
17
17
|
import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
|
|
18
18
|
import { emitHook } from './hooks.js';
|
|
19
|
+
import { checkDaemonStatus } from './browser/discover.js';
|
|
20
|
+
import { PKG_VERSION } from './version.js';
|
|
21
|
+
import chalk from 'chalk';
|
|
19
22
|
const _loadedModules = new Set();
|
|
20
23
|
export function coerceAndValidateArgs(cmdArgs, kwargs) {
|
|
21
24
|
const result = { ...kwargs };
|
|
@@ -107,6 +110,25 @@ function ensureRequiredEnv(cmd) {
|
|
|
107
110
|
return;
|
|
108
111
|
throw new CommandExecutionError(`Command ${fullName(cmd)} requires environment variable ${missing.name}.`, missing.help ?? `Set ${missing.name} before running ${fullName(cmd)}.`);
|
|
109
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if the browser is already on the target domain, avoiding redundant navigation.
|
|
115
|
+
* Returns true if current page hostname matches the pre-nav URL hostname.
|
|
116
|
+
*/
|
|
117
|
+
async function isAlreadyOnDomain(page, targetUrl) {
|
|
118
|
+
if (!page.getCurrentUrl)
|
|
119
|
+
return false;
|
|
120
|
+
try {
|
|
121
|
+
const currentUrl = await page.getCurrentUrl();
|
|
122
|
+
if (!currentUrl)
|
|
123
|
+
return false;
|
|
124
|
+
const currentHost = new URL(currentUrl).hostname;
|
|
125
|
+
const targetHost = new URL(targetUrl).hostname;
|
|
126
|
+
return currentHost === targetHost;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
110
132
|
export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
111
133
|
let kwargs;
|
|
112
134
|
try {
|
|
@@ -126,18 +148,43 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
|
126
148
|
let result;
|
|
127
149
|
try {
|
|
128
150
|
if (shouldUseBrowserSession(cmd)) {
|
|
151
|
+
// ── Fail-fast: only when daemon is UP but extension is not connected ──
|
|
152
|
+
// If daemon is not running, let browserSession() handle auto-start as usual.
|
|
153
|
+
// We only short-circuit when the daemon confirms the extension is missing —
|
|
154
|
+
// that's a clear setup gap, not a transient startup state.
|
|
155
|
+
// Use a short timeout: localhost responds in <50ms when running.
|
|
156
|
+
// 300ms avoids a full 2s wait on cold-start (daemon not yet running).
|
|
157
|
+
const status = await checkDaemonStatus({ timeout: 300 });
|
|
158
|
+
if (status.running && !status.extensionConnected) {
|
|
159
|
+
throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
|
|
160
|
+
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
161
|
+
' 2. chrome://extensions → Developer Mode → Load unpacked\n' +
|
|
162
|
+
' Then run: opencli doctor');
|
|
163
|
+
}
|
|
164
|
+
// ── Version mismatch: warn but don't block ──
|
|
165
|
+
if (status.extensionVersion && status.extensionVersion !== PKG_VERSION) {
|
|
166
|
+
process.stderr.write(chalk.yellow(`⚠ Extension v${status.extensionVersion} ≠ CLI v${PKG_VERSION} — consider updating the extension.\n`));
|
|
167
|
+
}
|
|
129
168
|
ensureRequiredEnv(cmd);
|
|
130
169
|
const BrowserFactory = getBrowserFactory();
|
|
131
170
|
result = await browserSession(BrowserFactory, async (page) => {
|
|
132
171
|
const preNavUrl = resolvePreNav(cmd);
|
|
133
172
|
if (preNavUrl) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await page.wait(2);
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
173
|
+
const skip = await isAlreadyOnDomain(page, preNavUrl);
|
|
174
|
+
if (skip) {
|
|
139
175
|
if (debug)
|
|
140
|
-
console.error(`[pre-nav]
|
|
176
|
+
console.error(`[pre-nav] Already on target domain, skipping navigation`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
try {
|
|
180
|
+
// goto() already includes smart DOM-settle detection (waitForDomStable).
|
|
181
|
+
// No additional fixed sleep needed.
|
|
182
|
+
await page.goto(preNavUrl);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
if (debug)
|
|
186
|
+
console.error(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
|
|
187
|
+
}
|
|
141
188
|
}
|
|
142
189
|
}
|
|
143
190
|
return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
|
|
@@ -147,7 +194,18 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
|
|
|
147
194
|
}, { workspace: `site:${cmd.site}` });
|
|
148
195
|
}
|
|
149
196
|
else {
|
|
150
|
-
|
|
197
|
+
// Non-browser commands: apply timeout only when explicitly configured.
|
|
198
|
+
const timeout = cmd.timeoutSeconds;
|
|
199
|
+
if (timeout !== undefined && timeout > 0) {
|
|
200
|
+
result = await runWithTimeout(runCommand(cmd, null, kwargs, debug), {
|
|
201
|
+
timeout,
|
|
202
|
+
label: fullName(cmd),
|
|
203
|
+
hint: `Increase the adapter's timeoutSeconds setting (currently ${timeout}s)`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
result = await runCommand(cmd, null, kwargs, debug);
|
|
208
|
+
}
|
|
151
209
|
}
|
|
152
210
|
}
|
|
153
211
|
catch (err) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { executeCommand } from './execution.js';
|
|
3
|
+
import { TimeoutError } from './errors.js';
|
|
4
|
+
import { cli, Strategy } from './registry.js';
|
|
5
|
+
import { withTimeoutMs } from './runtime.js';
|
|
6
|
+
describe('executeCommand — non-browser timeout', () => {
|
|
7
|
+
it('applies timeoutSeconds to non-browser commands', async () => {
|
|
8
|
+
const cmd = cli({
|
|
9
|
+
site: 'test-execution',
|
|
10
|
+
name: 'non-browser-timeout',
|
|
11
|
+
description: 'test non-browser timeout',
|
|
12
|
+
browser: false,
|
|
13
|
+
strategy: Strategy.PUBLIC,
|
|
14
|
+
timeoutSeconds: 0.01,
|
|
15
|
+
func: () => new Promise(() => { }),
|
|
16
|
+
});
|
|
17
|
+
// Sentinel timeout at 200ms — if the inner 10ms timeout fires first,
|
|
18
|
+
// the error will be a TimeoutError with the command label, not 'sentinel'.
|
|
19
|
+
const error = await withTimeoutMs(executeCommand(cmd, {}), 200, 'sentinel timeout')
|
|
20
|
+
.catch((err) => err);
|
|
21
|
+
expect(error).toBeInstanceOf(TimeoutError);
|
|
22
|
+
expect(error).toMatchObject({
|
|
23
|
+
code: 'TIMEOUT',
|
|
24
|
+
message: 'test-execution/non-browser-timeout timed out after 0.01s',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('skips timeout when timeoutSeconds is 0', async () => {
|
|
28
|
+
const cmd = cli({
|
|
29
|
+
site: 'test-execution',
|
|
30
|
+
name: 'non-browser-zero-timeout',
|
|
31
|
+
description: 'test zero timeout bypasses wrapping',
|
|
32
|
+
browser: false,
|
|
33
|
+
strategy: Strategy.PUBLIC,
|
|
34
|
+
timeoutSeconds: 0,
|
|
35
|
+
func: () => new Promise(() => { }),
|
|
36
|
+
});
|
|
37
|
+
// With timeout guard skipped, the sentinel fires instead.
|
|
38
|
+
await expect(withTimeoutMs(executeCommand(cmd, {}), 50, 'sentinel timeout')).rejects.toThrow('sentinel timeout');
|
|
39
|
+
});
|
|
40
|
+
});
|
package/dist/external.js
CHANGED
|
@@ -12,7 +12,10 @@ function getUserRegistryPath() {
|
|
|
12
12
|
const home = os.homedir();
|
|
13
13
|
return path.join(home, '.opencli', 'external-clis.yaml');
|
|
14
14
|
}
|
|
15
|
+
let _cachedExternalClis = null;
|
|
15
16
|
export function loadExternalClis() {
|
|
17
|
+
if (_cachedExternalClis)
|
|
18
|
+
return _cachedExternalClis;
|
|
16
19
|
const configs = new Map();
|
|
17
20
|
// 1. Load built-in
|
|
18
21
|
const builtinPath = path.resolve(__dirname, 'external-clis.yaml');
|
|
@@ -41,7 +44,8 @@ export function loadExternalClis() {
|
|
|
41
44
|
catch (err) {
|
|
42
45
|
log.warn(`Failed to parse user external-clis.yaml: ${getErrorMessage(err)}`);
|
|
43
46
|
}
|
|
44
|
-
|
|
47
|
+
_cachedExternalClis = Array.from(configs.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
return _cachedExternalClis;
|
|
45
49
|
}
|
|
46
50
|
export function isBinaryInstalled(binary) {
|
|
47
51
|
try {
|
|
@@ -200,5 +204,6 @@ export function registerExternalCli(name, opts) {
|
|
|
200
204
|
}
|
|
201
205
|
const dump = yaml.dump(items, { indent: 2, sortKeys: true });
|
|
202
206
|
fs.writeFileSync(userPath, dump, 'utf8');
|
|
207
|
+
_cachedExternalClis = null; // Invalidate cache so next load reflects the change
|
|
203
208
|
console.log(chalk.dim(userPath));
|
|
204
209
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -15,6 +15,8 @@ const _hooks = globalThis.__opencli_hooks__ ??= new Map();
|
|
|
15
15
|
// ── Registration API (used by plugins) ─────────────────────────────────────
|
|
16
16
|
function addHook(name, fn) {
|
|
17
17
|
const list = _hooks.get(name) ?? [];
|
|
18
|
+
if (list.includes(fn))
|
|
19
|
+
return;
|
|
18
20
|
list.push(fn);
|
|
19
21
|
_hooks.set(name, list);
|
|
20
22
|
}
|
package/dist/main.js
CHANGED
|
@@ -19,12 +19,18 @@ import { discoverClis, discoverPlugins } from './discovery.js';
|
|
|
19
19
|
import { getCompletions } from './completion.js';
|
|
20
20
|
import { runCli } from './cli.js';
|
|
21
21
|
import { emitHook } from './hooks.js';
|
|
22
|
+
import { registerUpdateNoticeOnExit, checkForUpdateBackground } from './update-check.js';
|
|
22
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
24
|
const __dirname = path.dirname(__filename);
|
|
24
25
|
const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
|
|
25
26
|
const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
27
|
+
// Sequential: plugins must run after built-in discovery so they can override built-in commands.
|
|
26
28
|
await discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
27
29
|
await discoverPlugins();
|
|
30
|
+
// Register exit hook: notice appears after command output (same as npm/gh/yarn)
|
|
31
|
+
registerUpdateNoticeOnExit();
|
|
32
|
+
// Kick off background fetch for next run (non-blocking)
|
|
33
|
+
checkForUpdateBackground();
|
|
28
34
|
// ── Fast-path: handle --get-completions before commander parses ─────────
|
|
29
35
|
// Usage: opencli --get-completions --cursor <N> [word1 word2 ...]
|
|
30
36
|
const getCompIdx = process.argv.indexOf('--get-completions');
|
package/dist/output.js
CHANGED
|
@@ -5,7 +5,11 @@ import chalk from 'chalk';
|
|
|
5
5
|
import Table from 'cli-table3';
|
|
6
6
|
import yaml from 'js-yaml';
|
|
7
7
|
function normalizeRows(data) {
|
|
8
|
-
|
|
8
|
+
if (Array.isArray(data))
|
|
9
|
+
return data;
|
|
10
|
+
if (data && typeof data === 'object')
|
|
11
|
+
return [data];
|
|
12
|
+
return [{ value: data }];
|
|
9
13
|
}
|
|
10
14
|
function resolveColumns(rows, opts) {
|
|
11
15
|
return opts.columns ?? Object.keys(rows[0] ?? {});
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
import { getStep } from './registry.js';
|
|
5
5
|
import { log } from '../logger.js';
|
|
6
6
|
import { ConfigError } from '../errors.js';
|
|
7
|
-
|
|
8
|
-
const BROWSER_STEPS = new Set(['navigate', 'evaluate', 'click', 'type', 'press', 'wait', 'snapshot']);
|
|
7
|
+
import { BROWSER_ONLY_STEPS } from '../capabilityRouting.js';
|
|
9
8
|
export async function executePipeline(page, pipeline, ctx = {}) {
|
|
10
9
|
const args = ctx.args ?? {};
|
|
11
10
|
const debug = ctx.debug ?? false;
|
|
@@ -33,7 +32,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
|
|
|
33
32
|
}
|
|
34
33
|
catch (err) {
|
|
35
34
|
// Attempt cleanup: close automation window on pipeline failure
|
|
36
|
-
if (page
|
|
35
|
+
if (page?.closeWindow) {
|
|
37
36
|
try {
|
|
38
37
|
await page.closeWindow();
|
|
39
38
|
}
|
|
@@ -44,7 +43,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
|
|
|
44
43
|
return data;
|
|
45
44
|
}
|
|
46
45
|
async function executeStepWithRetry(handler, page, params, data, args, op, configRetries) {
|
|
47
|
-
const maxRetries = configRetries ?? (
|
|
46
|
+
const maxRetries = configRetries ?? (BROWSER_ONLY_STEPS.has(op) ? 2 : 0);
|
|
48
47
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
49
48
|
try {
|
|
50
49
|
return await handler(page, params, data, args);
|