@jackwener/opencli 1.7.7 → 1.7.9
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/README.md +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +3 -22
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +36 -9
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +29 -9
- package/dist/src/commanderAdapter.test.js +78 -2
- package/dist/src/commands/daemon.js +6 -0
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveAdapterSourcePath } from './adapter-source.js';
|
|
3
|
+
function makeCmd(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
site: 'test-site',
|
|
6
|
+
name: 'test-cmd',
|
|
7
|
+
description: 'test',
|
|
8
|
+
args: [],
|
|
9
|
+
...overrides,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe('resolveAdapterSourcePath', () => {
|
|
13
|
+
it('returns source when it is a real file path (not manifest:)', () => {
|
|
14
|
+
const cmd = makeCmd({ source: '/home/user/.opencli/clis/arxiv/search.js' });
|
|
15
|
+
expect(resolveAdapterSourcePath(cmd)).toBe('/home/user/.opencli/clis/arxiv/search.js');
|
|
16
|
+
});
|
|
17
|
+
it('skips manifest: pseudo-paths and falls back to _modulePath', () => {
|
|
18
|
+
const cmd = makeCmd({ source: 'manifest:arxiv/search', _modulePath: '/pkg/clis/arxiv/search.js' });
|
|
19
|
+
expect(resolveAdapterSourcePath(cmd)).toBe('/pkg/clis/arxiv/search.js');
|
|
20
|
+
});
|
|
21
|
+
it('returns undefined when only manifest: pseudo-path and no _modulePath', () => {
|
|
22
|
+
const cmd = makeCmd({ source: 'manifest:test/cmd' });
|
|
23
|
+
expect(resolveAdapterSourcePath(cmd)).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('returns _modulePath when it is the only path available', () => {
|
|
26
|
+
const cmd = makeCmd({ _modulePath: '/project/clis/site/cmd.js' });
|
|
27
|
+
expect(resolveAdapterSourcePath(cmd)).toBe('/project/clis/site/cmd.js');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Subclasses implement the transport-specific methods: goto, evaluate,
|
|
9
9
|
* getCookies, screenshot, tabs, etc.
|
|
10
10
|
*/
|
|
11
|
-
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
11
|
+
import type { BrowserCookie, FetchJsonOptions, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
12
12
|
import { type ResolveOptions, type TargetMatchLevel } from './target-resolver.js';
|
|
13
13
|
export interface ResolveSuccess {
|
|
14
14
|
matches_n: number;
|
|
@@ -26,6 +26,7 @@ export declare abstract class BasePage implements IPage {
|
|
|
26
26
|
abstract goto(url: string, options?: {
|
|
27
27
|
waitUntil?: 'load' | 'none';
|
|
28
28
|
settleMs?: number;
|
|
29
|
+
allowBoundNavigation?: boolean;
|
|
29
30
|
}): Promise<void>;
|
|
30
31
|
abstract evaluate(js: string): Promise<unknown>;
|
|
31
32
|
/**
|
|
@@ -37,6 +38,7 @@ export declare abstract class BasePage implements IPage {
|
|
|
37
38
|
* page.evaluateWithArgs(`(async () => { return sym; })()`, { sym: userInput })
|
|
38
39
|
*/
|
|
39
40
|
evaluateWithArgs(js: string, args: Record<string, unknown>): Promise<unknown>;
|
|
41
|
+
fetchJson(url: string, opts?: FetchJsonOptions): Promise<unknown>;
|
|
40
42
|
abstract getCookies(opts?: {
|
|
41
43
|
domain?: string;
|
|
42
44
|
url?: string;
|
|
@@ -12,6 +12,8 @@ import { generateSnapshotJs, getFormStateJs } from './dom-snapshot.js';
|
|
|
12
12
|
import { pressKeyJs, waitForTextJs, waitForCaptureJs, waitForSelectorJs, scrollJs, autoScrollJs, networkRequestsJs, waitForDomStableJs, } from './dom-helpers.js';
|
|
13
13
|
import { resolveTargetJs, clickResolvedJs, typeResolvedJs, scrollResolvedJs, } from './target-resolver.js';
|
|
14
14
|
import { TargetError } from './target-errors.js';
|
|
15
|
+
import { CliError } from '../errors.js';
|
|
16
|
+
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
15
17
|
/**
|
|
16
18
|
* Execute `resolveTargetJs` once, throw structured `TargetError` on failure.
|
|
17
19
|
* Single helper so click/typeText/scrollTo share one resolution pathway,
|
|
@@ -30,7 +32,10 @@ async function runResolve(page, ref, opts = {}) {
|
|
|
30
32
|
}
|
|
31
33
|
return { matches_n: resolution.matches_n, match_level: resolution.match_level };
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
function previewText(text) {
|
|
36
|
+
const preview = (text ?? '').replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
37
|
+
return preview ? `Response preview: ${preview}` : undefined;
|
|
38
|
+
}
|
|
34
39
|
export class BasePage {
|
|
35
40
|
_lastUrl = null;
|
|
36
41
|
/** Cached previous snapshot hashes for incremental diff marking */
|
|
@@ -54,6 +59,76 @@ export class BasePage {
|
|
|
54
59
|
.join('\n');
|
|
55
60
|
return this.evaluate(`${declarations}\n${js}`);
|
|
56
61
|
}
|
|
62
|
+
async fetchJson(url, opts = {}) {
|
|
63
|
+
const request = {
|
|
64
|
+
url,
|
|
65
|
+
method: opts.method ?? 'GET',
|
|
66
|
+
headers: opts.headers ?? {},
|
|
67
|
+
body: opts.body,
|
|
68
|
+
hasBody: opts.body !== undefined,
|
|
69
|
+
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
70
|
+
};
|
|
71
|
+
const result = await this.evaluateWithArgs(`
|
|
72
|
+
(async () => {
|
|
73
|
+
const ctrl = new AbortController();
|
|
74
|
+
const timer = setTimeout(() => ctrl.abort(), request.timeoutMs);
|
|
75
|
+
try {
|
|
76
|
+
const headers = { Accept: 'application/json', ...request.headers };
|
|
77
|
+
const init = {
|
|
78
|
+
method: request.method,
|
|
79
|
+
credentials: 'include',
|
|
80
|
+
headers,
|
|
81
|
+
signal: ctrl.signal,
|
|
82
|
+
};
|
|
83
|
+
if (request.hasBody) {
|
|
84
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === 'content-type')) {
|
|
85
|
+
headers['Content-Type'] = 'application/json';
|
|
86
|
+
}
|
|
87
|
+
init.body = JSON.stringify(request.body);
|
|
88
|
+
}
|
|
89
|
+
const resp = await fetch(request.url, init);
|
|
90
|
+
const text = await resp.text();
|
|
91
|
+
return {
|
|
92
|
+
ok: resp.ok,
|
|
93
|
+
status: resp.status,
|
|
94
|
+
statusText: resp.statusText,
|
|
95
|
+
url: resp.url,
|
|
96
|
+
contentType: resp.headers.get('content-type') || '',
|
|
97
|
+
text,
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
status: 0,
|
|
103
|
+
statusText: '',
|
|
104
|
+
url: request.url,
|
|
105
|
+
contentType: '',
|
|
106
|
+
text: '',
|
|
107
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
};
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
})()
|
|
113
|
+
`, { request });
|
|
114
|
+
const targetUrl = result.url || url;
|
|
115
|
+
if (result.error) {
|
|
116
|
+
throw new CliError('FETCH_ERROR', `Browser fetch failed for ${targetUrl}: ${result.error}`, 'Check that the page is reachable and the current browser profile has access.');
|
|
117
|
+
}
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
throw new CliError('FETCH_ERROR', `HTTP ${result.status ?? 0}${result.statusText ? ` ${result.statusText}` : ''} from ${targetUrl}`, previewText(result.text));
|
|
120
|
+
}
|
|
121
|
+
const text = result.text ?? '';
|
|
122
|
+
if (!text.trim())
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(text);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
const contentType = result.contentType ? ` (${result.contentType})` : '';
|
|
129
|
+
throw new CliError('FETCH_ERROR', `Expected JSON from ${targetUrl}${contentType}`, previewText(text));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
57
132
|
// ── Shared DOM helper implementations ──
|
|
58
133
|
async click(ref, opts = {}) {
|
|
59
134
|
// Phase 1: Resolve target with fingerprint verification
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CliError } from '../errors.js';
|
|
3
|
+
import { BasePage } from './base-page.js';
|
|
4
|
+
class TestPage extends BasePage {
|
|
5
|
+
result;
|
|
6
|
+
args;
|
|
7
|
+
async goto() { }
|
|
8
|
+
async evaluate() { return null; }
|
|
9
|
+
async evaluateWithArgs(_js, args) {
|
|
10
|
+
this.args = args;
|
|
11
|
+
return this.result;
|
|
12
|
+
}
|
|
13
|
+
async getCookies() { return []; }
|
|
14
|
+
async screenshot() { return ''; }
|
|
15
|
+
async tabs() { return []; }
|
|
16
|
+
async selectTab() { }
|
|
17
|
+
}
|
|
18
|
+
describe('BasePage.fetchJson', () => {
|
|
19
|
+
it('passes a narrow browser-context JSON request and parses the response in Node', async () => {
|
|
20
|
+
const page = new TestPage();
|
|
21
|
+
page.result = {
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
url: 'https://api.example.com/items',
|
|
25
|
+
contentType: 'application/json',
|
|
26
|
+
text: '{"items":[1]}',
|
|
27
|
+
};
|
|
28
|
+
await expect(page.fetchJson('https://api.example.com/items', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'X-Test': '1' },
|
|
31
|
+
body: { q: 'opencli' },
|
|
32
|
+
timeoutMs: 1234,
|
|
33
|
+
})).resolves.toEqual({ items: [1] });
|
|
34
|
+
expect(page.args).toEqual({
|
|
35
|
+
request: {
|
|
36
|
+
url: 'https://api.example.com/items',
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'X-Test': '1' },
|
|
39
|
+
body: { q: 'opencli' },
|
|
40
|
+
hasBody: true,
|
|
41
|
+
timeoutMs: 1234,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it('throws a CliError for non-JSON responses', async () => {
|
|
46
|
+
const page = new TestPage();
|
|
47
|
+
page.result = {
|
|
48
|
+
ok: true,
|
|
49
|
+
status: 200,
|
|
50
|
+
url: 'https://api.example.com/items',
|
|
51
|
+
contentType: 'text/html',
|
|
52
|
+
text: '<html>blocked</html>',
|
|
53
|
+
};
|
|
54
|
+
const err = await page.fetchJson('https://api.example.com/items').catch((error) => error);
|
|
55
|
+
expect(err).toBeInstanceOf(CliError);
|
|
56
|
+
expect(err.code).toBe('FETCH_ERROR');
|
|
57
|
+
expect(err.message).toContain('Expected JSON');
|
|
58
|
+
expect(err.hint).toContain('blocked');
|
|
59
|
+
});
|
|
60
|
+
it('throws a CliError for browser fetch transport errors', async () => {
|
|
61
|
+
const page = new TestPage();
|
|
62
|
+
page.result = {
|
|
63
|
+
ok: false,
|
|
64
|
+
status: 0,
|
|
65
|
+
url: 'https://api.example.com/items',
|
|
66
|
+
text: '',
|
|
67
|
+
error: 'The operation was aborted.',
|
|
68
|
+
};
|
|
69
|
+
await expect(page.fetchJson('https://api.example.com/items')).rejects.toMatchObject({
|
|
70
|
+
code: 'FETCH_ERROR',
|
|
71
|
+
message: expect.stringContaining('The operation was aborted.'),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -10,6 +10,7 @@ import { getDaemonHealth, requestDaemonShutdown } from './daemon-client.js';
|
|
|
10
10
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
11
11
|
import { BrowserConnectError } from '../errors.js';
|
|
12
12
|
import { PKG_VERSION } from '../version.js';
|
|
13
|
+
import { resolveProfileContextId } from './profile.js';
|
|
13
14
|
const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension
|
|
14
15
|
/**
|
|
15
16
|
* Browser factory: manages daemon lifecycle and provides IPage instances.
|
|
@@ -32,8 +33,9 @@ export class BrowserBridge {
|
|
|
32
33
|
throw new Error('Session is closed');
|
|
33
34
|
this._state = 'connecting';
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
-
this.
|
|
36
|
+
const contextId = opts.contextId ?? resolveProfileContextId();
|
|
37
|
+
await this._ensureDaemon(opts.timeout, contextId);
|
|
38
|
+
this._page = new Page(opts.workspace, opts.idleTimeout, contextId);
|
|
37
39
|
this._state = 'connected';
|
|
38
40
|
return this._page;
|
|
39
41
|
}
|
|
@@ -51,13 +53,21 @@ export class BrowserBridge {
|
|
|
51
53
|
this._page = null;
|
|
52
54
|
this._state = 'closed';
|
|
53
55
|
}
|
|
54
|
-
async _ensureDaemon(timeoutSeconds) {
|
|
56
|
+
async _ensureDaemon(timeoutSeconds, contextId) {
|
|
55
57
|
const effectiveSeconds = (timeoutSeconds && timeoutSeconds > 0) ? timeoutSeconds : Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000);
|
|
56
58
|
const timeoutMs = effectiveSeconds * 1000;
|
|
57
|
-
const health = await getDaemonHealth();
|
|
59
|
+
const health = await getDaemonHealth({ contextId });
|
|
58
60
|
// Fast path: everything ready
|
|
59
61
|
if (health.state === 'ready')
|
|
60
62
|
return;
|
|
63
|
+
if (health.state === 'profile-required') {
|
|
64
|
+
throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
|
|
65
|
+
'Run opencli profile list to see connected profiles.', 'profile-required');
|
|
66
|
+
}
|
|
67
|
+
if (health.state === 'profile-disconnected') {
|
|
68
|
+
const label = contextId ?? health.status.contextId ?? 'unknown';
|
|
69
|
+
throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
|
|
70
|
+
}
|
|
61
71
|
// Daemon running but no extension
|
|
62
72
|
if (health.state === 'no-extension') {
|
|
63
73
|
// Detect stale daemon: version mismatch OR missing daemonVersion (pre-version daemon)
|
|
@@ -86,8 +96,17 @@ export class BrowserBridge {
|
|
|
86
96
|
process.stderr.write('⏳ Waiting for Chrome/Chromium extension to connect...\n');
|
|
87
97
|
process.stderr.write(' Make sure Chrome or Chromium is open and the OpenCLI extension is enabled.\n');
|
|
88
98
|
}
|
|
89
|
-
if (await this._pollUntilReady(timeoutMs))
|
|
99
|
+
if (await this._pollUntilReady(timeoutMs, contextId))
|
|
90
100
|
return;
|
|
101
|
+
const finalHealth = await getDaemonHealth({ contextId });
|
|
102
|
+
if (finalHealth.state === 'profile-required') {
|
|
103
|
+
throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
|
|
104
|
+
'Run opencli profile list to see connected profiles.', 'profile-required');
|
|
105
|
+
}
|
|
106
|
+
if (finalHealth.state === 'profile-disconnected') {
|
|
107
|
+
const label = contextId ?? finalHealth.status.contextId ?? 'unknown';
|
|
108
|
+
throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
|
|
109
|
+
}
|
|
91
110
|
throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
|
|
92
111
|
'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
|
|
93
112
|
'If not installed:\n' +
|
|
@@ -115,9 +134,17 @@ export class BrowserBridge {
|
|
|
115
134
|
});
|
|
116
135
|
this._daemonProc.unref();
|
|
117
136
|
// Wait for daemon + extension
|
|
118
|
-
if (await this._pollUntilReady(timeoutMs))
|
|
137
|
+
if (await this._pollUntilReady(timeoutMs, contextId))
|
|
119
138
|
return;
|
|
120
|
-
const finalHealth = await getDaemonHealth();
|
|
139
|
+
const finalHealth = await getDaemonHealth({ contextId });
|
|
140
|
+
if (finalHealth.state === 'profile-required') {
|
|
141
|
+
throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
|
|
142
|
+
'Run opencli profile list to see connected profiles.', 'profile-required');
|
|
143
|
+
}
|
|
144
|
+
if (finalHealth.state === 'profile-disconnected') {
|
|
145
|
+
const label = contextId ?? finalHealth.status.contextId ?? 'unknown';
|
|
146
|
+
throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
|
|
147
|
+
}
|
|
121
148
|
if (finalHealth.state === 'no-extension') {
|
|
122
149
|
throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
|
|
123
150
|
'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
|
|
@@ -139,11 +166,11 @@ export class BrowserBridge {
|
|
|
139
166
|
return false;
|
|
140
167
|
}
|
|
141
168
|
/** Poll getDaemonHealth() until state is 'ready' or deadline is reached. */
|
|
142
|
-
async _pollUntilReady(timeoutMs) {
|
|
169
|
+
async _pollUntilReady(timeoutMs, contextId) {
|
|
143
170
|
const deadline = Date.now() + timeoutMs;
|
|
144
171
|
while (Date.now() < deadline) {
|
|
145
172
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
146
|
-
const h = await getDaemonHealth();
|
|
173
|
+
const h = await getDaemonHealth({ contextId });
|
|
147
174
|
if (h.state === 'ready')
|
|
148
175
|
return true;
|
|
149
176
|
}
|
|
@@ -25,6 +25,7 @@ export declare class CDPBridge implements IBrowserFactory {
|
|
|
25
25
|
timeout?: number;
|
|
26
26
|
workspace?: string;
|
|
27
27
|
cdpEndpoint?: string;
|
|
28
|
+
contextId?: string;
|
|
28
29
|
}): Promise<IPage>;
|
|
29
30
|
close(): Promise<void>;
|
|
30
31
|
send(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -229,7 +229,7 @@ class CDPPage extends BasePage {
|
|
|
229
229
|
const idx = this._networkEntries.push({
|
|
230
230
|
url: p.request.url,
|
|
231
231
|
method: p.request.method,
|
|
232
|
-
timestamp:
|
|
232
|
+
timestamp: Date.now(),
|
|
233
233
|
}) - 1;
|
|
234
234
|
this._pendingRequests.set(p.requestId, idx);
|
|
235
235
|
}
|
|
@@ -290,7 +290,7 @@ class CDPPage extends BasePage {
|
|
|
290
290
|
this.bridge.on('Runtime.consoleAPICalled', (params) => {
|
|
291
291
|
const p = params;
|
|
292
292
|
const text = (p.args || []).map(a => a.value !== undefined ? String(a.value) : (a.description || '')).join(' ');
|
|
293
|
-
this._consoleMessages.push({ type: p.type, text, timestamp:
|
|
293
|
+
this._consoleMessages.push({ type: p.type, text, timestamp: Date.now() });
|
|
294
294
|
if (this._consoleMessages.length > 500)
|
|
295
295
|
this._consoleMessages.shift();
|
|
296
296
|
});
|
|
@@ -298,7 +298,7 @@ class CDPPage extends BasePage {
|
|
|
298
298
|
this.bridge.on('Runtime.exceptionThrown', (params) => {
|
|
299
299
|
const p = params;
|
|
300
300
|
const desc = p.exceptionDetails?.exception?.description || p.exceptionDetails?.text || 'Unknown exception';
|
|
301
|
-
this._consoleMessages.push({ type: 'error', text: desc, timestamp:
|
|
301
|
+
this._consoleMessages.push({ type: 'error', text: desc, timestamp: Date.now() });
|
|
302
302
|
if (this._consoleMessages.length > 500)
|
|
303
303
|
this._consoleMessages.shift();
|
|
304
304
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { BrowserSessionInfo } from '../types.js';
|
|
7
7
|
export interface DaemonCommand {
|
|
8
8
|
id: string;
|
|
9
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind
|
|
9
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'cdp' | 'frames';
|
|
10
10
|
/** Target page identity (targetId). Cross-layer contract with the extension. */
|
|
11
11
|
page?: string;
|
|
12
12
|
code?: string;
|
|
@@ -30,21 +30,32 @@ export interface DaemonCommand {
|
|
|
30
30
|
pattern?: string;
|
|
31
31
|
cdpMethod?: string;
|
|
32
32
|
cdpParams?: Record<string, unknown>;
|
|
33
|
-
/** When true, automation
|
|
33
|
+
/** When true, the owned automation container is created in the foreground */
|
|
34
34
|
windowFocused?: boolean;
|
|
35
35
|
/** Custom idle timeout in seconds for this workspace session. Overrides the default. */
|
|
36
36
|
idleTimeout?: number;
|
|
37
|
+
/** Explicitly allow navigation inside a borrowed bound tab. */
|
|
38
|
+
allowBoundNavigation?: boolean;
|
|
37
39
|
/** Frame index for cross-frame operations (0-based, from 'frames' action) */
|
|
38
40
|
frameIndex?: number;
|
|
41
|
+
/** Browser profile/context to route the command to. */
|
|
42
|
+
contextId?: string;
|
|
39
43
|
}
|
|
40
44
|
export interface DaemonResult {
|
|
41
45
|
id: string;
|
|
42
46
|
ok: boolean;
|
|
43
47
|
data?: unknown;
|
|
44
48
|
error?: string;
|
|
49
|
+
errorCode?: string;
|
|
50
|
+
errorHint?: string;
|
|
45
51
|
/** Page identity (targetId) — present on page-scoped command responses */
|
|
46
52
|
page?: string;
|
|
47
53
|
}
|
|
54
|
+
export declare class BrowserCommandError extends Error {
|
|
55
|
+
readonly code?: string | undefined;
|
|
56
|
+
readonly hint?: string | undefined;
|
|
57
|
+
constructor(message: string, code?: string | undefined, hint?: string | undefined);
|
|
58
|
+
}
|
|
48
59
|
export interface DaemonStatus {
|
|
49
60
|
ok: boolean;
|
|
50
61
|
pid: number;
|
|
@@ -53,12 +64,25 @@ export interface DaemonStatus {
|
|
|
53
64
|
extensionConnected: boolean;
|
|
54
65
|
extensionVersion?: string;
|
|
55
66
|
extensionCompatRange?: string;
|
|
67
|
+
contextId?: string;
|
|
68
|
+
profileRequired?: boolean;
|
|
69
|
+
profileDisconnected?: boolean;
|
|
70
|
+
profiles?: BrowserProfileStatus[];
|
|
56
71
|
pending: number;
|
|
57
72
|
memoryMB: number;
|
|
58
73
|
port: number;
|
|
59
74
|
}
|
|
75
|
+
export interface BrowserProfileStatus {
|
|
76
|
+
contextId: string;
|
|
77
|
+
extensionConnected: boolean;
|
|
78
|
+
extensionVersion?: string;
|
|
79
|
+
extensionCompatRange?: string;
|
|
80
|
+
pending: number;
|
|
81
|
+
lastSeenAt?: number;
|
|
82
|
+
}
|
|
60
83
|
export declare function fetchDaemonStatus(opts?: {
|
|
61
84
|
timeout?: number;
|
|
85
|
+
contextId?: string;
|
|
62
86
|
}): Promise<DaemonStatus | null>;
|
|
63
87
|
export type DaemonHealth = {
|
|
64
88
|
state: 'stopped';
|
|
@@ -66,6 +90,12 @@ export type DaemonHealth = {
|
|
|
66
90
|
} | {
|
|
67
91
|
state: 'no-extension';
|
|
68
92
|
status: DaemonStatus;
|
|
93
|
+
} | {
|
|
94
|
+
state: 'profile-required';
|
|
95
|
+
status: DaemonStatus;
|
|
96
|
+
} | {
|
|
97
|
+
state: 'profile-disconnected';
|
|
98
|
+
status: DaemonStatus;
|
|
69
99
|
} | {
|
|
70
100
|
state: 'ready';
|
|
71
101
|
status: DaemonStatus;
|
|
@@ -76,6 +106,7 @@ export type DaemonHealth = {
|
|
|
76
106
|
*/
|
|
77
107
|
export declare function getDaemonHealth(opts?: {
|
|
78
108
|
timeout?: number;
|
|
109
|
+
contextId?: string;
|
|
79
110
|
}): Promise<DaemonHealth>;
|
|
80
111
|
export declare function requestDaemonShutdown(opts?: {
|
|
81
112
|
timeout?: number;
|
|
@@ -92,8 +123,11 @@ export declare function sendCommandFull(action: DaemonCommand['action'], params?
|
|
|
92
123
|
data: unknown;
|
|
93
124
|
page?: string;
|
|
94
125
|
}>;
|
|
95
|
-
export declare function listSessions(
|
|
96
|
-
|
|
126
|
+
export declare function listSessions(opts?: {
|
|
127
|
+
contextId?: string;
|
|
128
|
+
}): Promise<BrowserSessionInfo[]>;
|
|
129
|
+
export declare function bindTab(workspace: string, opts?: {
|
|
97
130
|
matchDomain?: string;
|
|
98
131
|
matchPathPrefix?: string;
|
|
132
|
+
contextId?: string;
|
|
99
133
|
}): Promise<unknown>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
7
7
|
import { sleep } from '../utils.js';
|
|
8
8
|
import { classifyBrowserError } from './errors.js';
|
|
9
|
+
import { resolveProfileContextId } from './profile.js';
|
|
9
10
|
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
10
11
|
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
11
12
|
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
|
|
@@ -13,6 +14,16 @@ let _idCounter = 0;
|
|
|
13
14
|
function generateId() {
|
|
14
15
|
return `cmd_${process.pid}_${Date.now()}_${++_idCounter}`;
|
|
15
16
|
}
|
|
17
|
+
export class BrowserCommandError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
hint;
|
|
20
|
+
constructor(message, code, hint) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.hint = hint;
|
|
24
|
+
this.name = 'BrowserCommandError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
async function requestDaemon(pathname, init) {
|
|
17
28
|
const { timeout = 2000, headers, ...rest } = init ?? {};
|
|
18
29
|
const controller = new AbortController();
|
|
@@ -30,7 +41,8 @@ async function requestDaemon(pathname, init) {
|
|
|
30
41
|
}
|
|
31
42
|
export async function fetchDaemonStatus(opts) {
|
|
32
43
|
try {
|
|
33
|
-
const
|
|
44
|
+
const params = opts?.contextId ? `?contextId=${encodeURIComponent(opts.contextId)}` : '';
|
|
45
|
+
const res = await requestDaemon(`/status${params}`, { timeout: opts?.timeout ?? 2000 });
|
|
34
46
|
if (!res.ok)
|
|
35
47
|
return null;
|
|
36
48
|
return await res.json();
|
|
@@ -47,6 +59,10 @@ export async function getDaemonHealth(opts) {
|
|
|
47
59
|
const status = await fetchDaemonStatus(opts);
|
|
48
60
|
if (!status)
|
|
49
61
|
return { state: 'stopped', status: null };
|
|
62
|
+
if (status.profileRequired)
|
|
63
|
+
return { state: 'profile-required', status };
|
|
64
|
+
if (status.profileDisconnected)
|
|
65
|
+
return { state: 'profile-disconnected', status };
|
|
50
66
|
if (!status.extensionConnected)
|
|
51
67
|
return { state: 'no-extension', status };
|
|
52
68
|
return { state: 'ready', status };
|
|
@@ -75,7 +91,8 @@ async function sendCommandRaw(action, params) {
|
|
|
75
91
|
const id = generateId();
|
|
76
92
|
const wf = process.env.OPENCLI_WINDOW_FOCUSED;
|
|
77
93
|
const windowFocused = (wf === '1' || wf === 'true') ? true : undefined;
|
|
78
|
-
const
|
|
94
|
+
const contextId = params.contextId ?? resolveProfileContextId();
|
|
95
|
+
const command = { id, action, ...params, ...(contextId && { contextId }), ...(windowFocused && { windowFocused }) };
|
|
79
96
|
try {
|
|
80
97
|
const res = await requestDaemon('/command', {
|
|
81
98
|
method: 'POST',
|
|
@@ -95,7 +112,7 @@ async function sendCommandRaw(action, params) {
|
|
|
95
112
|
await sleep(advice.delayMs);
|
|
96
113
|
continue;
|
|
97
114
|
}
|
|
98
|
-
throw new
|
|
115
|
+
throw new BrowserCommandError(result.error ?? 'Daemon command failed', result.errorCode, result.errorHint);
|
|
99
116
|
}
|
|
100
117
|
return result;
|
|
101
118
|
}
|
|
@@ -126,10 +143,10 @@ export async function sendCommandFull(action, params = {}) {
|
|
|
126
143
|
const result = await sendCommandRaw(action, params);
|
|
127
144
|
return { data: result.data, page: result.page };
|
|
128
145
|
}
|
|
129
|
-
export async function listSessions() {
|
|
130
|
-
const result = await sendCommand('sessions');
|
|
146
|
+
export async function listSessions(opts) {
|
|
147
|
+
const result = await sendCommand('sessions', { ...(opts?.contextId && { contextId: opts.contextId }) });
|
|
131
148
|
return Array.isArray(result) ? result : [];
|
|
132
149
|
}
|
|
133
|
-
export async function
|
|
134
|
-
return sendCommand('bind
|
|
150
|
+
export async function bindTab(workspace, opts = {}) {
|
|
151
|
+
return sendCommand('bind', { workspace, ...opts });
|
|
135
152
|
}
|
|
@@ -6,6 +6,7 @@ describe('daemon-client', () => {
|
|
|
6
6
|
});
|
|
7
7
|
afterEach(() => {
|
|
8
8
|
vi.restoreAllMocks();
|
|
9
|
+
vi.unstubAllEnvs();
|
|
9
10
|
});
|
|
10
11
|
it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
|
|
11
12
|
const status = {
|
|
@@ -78,6 +79,43 @@ describe('daemon-client', () => {
|
|
|
78
79
|
});
|
|
79
80
|
await expect(getDaemonHealth()).resolves.toEqual({ state: 'ready', status });
|
|
80
81
|
});
|
|
82
|
+
it('getDaemonHealth returns profile-required when multiple profiles are connected without a selection', async () => {
|
|
83
|
+
const status = {
|
|
84
|
+
ok: true,
|
|
85
|
+
pid: 123,
|
|
86
|
+
uptime: 10,
|
|
87
|
+
extensionConnected: false,
|
|
88
|
+
profileRequired: true,
|
|
89
|
+
profiles: [
|
|
90
|
+
{ contextId: 'work', extensionConnected: true, pending: 0 },
|
|
91
|
+
{ contextId: 'personal', extensionConnected: true, pending: 0 },
|
|
92
|
+
],
|
|
93
|
+
pending: 0,
|
|
94
|
+
memoryMB: 32,
|
|
95
|
+
port: 19825,
|
|
96
|
+
};
|
|
97
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
98
|
+
ok: true,
|
|
99
|
+
json: () => Promise.resolve(status),
|
|
100
|
+
});
|
|
101
|
+
await expect(getDaemonHealth()).resolves.toEqual({ state: 'profile-required', status });
|
|
102
|
+
});
|
|
103
|
+
it('fetchDaemonStatus includes contextId in the status query', async () => {
|
|
104
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: () => Promise.resolve({
|
|
107
|
+
ok: true,
|
|
108
|
+
pid: 1,
|
|
109
|
+
uptime: 0,
|
|
110
|
+
extensionConnected: true,
|
|
111
|
+
pending: 0,
|
|
112
|
+
memoryMB: 1,
|
|
113
|
+
port: 19825,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
await fetchDaemonStatus({ contextId: 'work' });
|
|
117
|
+
expect(vi.mocked(fetch).mock.calls[0][0]).toMatch(/\/status\?contextId=work$/);
|
|
118
|
+
});
|
|
81
119
|
it('sendCommand includes the current pid in generated command ids', async () => {
|
|
82
120
|
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_000);
|
|
83
121
|
vi.mocked(fetch).mockResolvedValue({
|
|
@@ -95,6 +133,17 @@ describe('daemon-client', () => {
|
|
|
95
133
|
expect(ids[1]).toMatch(new RegExp(`^cmd_${process.pid}_1763000000000_\\d+$`));
|
|
96
134
|
expect(ids[0]).not.toBe(ids[1]);
|
|
97
135
|
});
|
|
136
|
+
it('sendCommand forwards OPENCLI_PROFILE as command contextId', async () => {
|
|
137
|
+
vi.stubEnv('OPENCLI_PROFILE', 'work');
|
|
138
|
+
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_000);
|
|
139
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
140
|
+
status: 200,
|
|
141
|
+
json: () => Promise.resolve({ id: 'server', ok: true, data: 'ok' }),
|
|
142
|
+
});
|
|
143
|
+
await sendCommand('exec', { code: '1 + 1' });
|
|
144
|
+
const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body));
|
|
145
|
+
expect(body.contextId).toBe('work');
|
|
146
|
+
});
|
|
98
147
|
it('sendCommand retries with a new id when daemon reports a duplicate pending id', async () => {
|
|
99
148
|
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_123);
|
|
100
149
|
const fetchMock = vi.mocked(fetch);
|
|
@@ -15,7 +15,10 @@ const EXTENSION_TRANSIENT_PATTERNS = [
|
|
|
15
15
|
'Extension disconnected',
|
|
16
16
|
'Extension not connected',
|
|
17
17
|
'attach failed',
|
|
18
|
+
'Detached while handling command',
|
|
19
|
+
'Debugger is not attached to the tab',
|
|
18
20
|
'no longer exists',
|
|
21
|
+
'No tab with id',
|
|
19
22
|
'CDP connection',
|
|
20
23
|
'Daemon command failed',
|
|
21
24
|
'No window with id',
|
|
@@ -6,7 +6,10 @@ describe('classifyBrowserError', () => {
|
|
|
6
6
|
'Extension disconnected',
|
|
7
7
|
'Extension not connected',
|
|
8
8
|
'attach failed',
|
|
9
|
+
'Detached while handling command',
|
|
10
|
+
'Debugger is not attached to the tab: 123',
|
|
9
11
|
'no longer exists',
|
|
12
|
+
'No tab with id: 456',
|
|
10
13
|
'CDP connection reset',
|
|
11
14
|
'Daemon command failed',
|
|
12
15
|
'No window with id: 123',
|