@jackwener/opencli 1.7.17 → 1.7.19

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.
Files changed (118) hide show
  1. package/README.md +10 -8
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +585 -9
  4. package/clis/ctrip/ctrip.test.js +486 -1
  5. package/clis/ctrip/flight.js +136 -0
  6. package/clis/ctrip/hotel-search.js +132 -0
  7. package/clis/ctrip/utils.js +298 -0
  8. package/clis/doubao/utils.js +17 -0
  9. package/clis/doubao/utils.test.js +61 -0
  10. package/clis/google/search.js +16 -6
  11. package/clis/google-scholar/search.js +20 -5
  12. package/clis/google-scholar/search.test.js +35 -2
  13. package/clis/reddit/home.js +117 -0
  14. package/clis/reddit/home.test.js +127 -0
  15. package/clis/reddit/read.js +400 -54
  16. package/clis/reddit/read.test.js +315 -12
  17. package/clis/reddit/reply.js +182 -0
  18. package/clis/reddit/reply.test.js +89 -0
  19. package/clis/reddit/subreddit-info.js +117 -0
  20. package/clis/reddit/subreddit-info.test.js +163 -0
  21. package/clis/reddit/whoami.js +84 -0
  22. package/clis/reddit/whoami.test.js +105 -0
  23. package/clis/rednote/comments.js +76 -0
  24. package/clis/rednote/download.js +59 -0
  25. package/clis/rednote/feed.js +95 -0
  26. package/clis/rednote/navigation.test.js +26 -0
  27. package/clis/rednote/note.js +68 -0
  28. package/clis/rednote/notifications.js +139 -0
  29. package/clis/rednote/rednote.test.js +157 -0
  30. package/clis/rednote/search.js +101 -0
  31. package/clis/rednote/user.js +55 -0
  32. package/clis/twitter/bookmark-folder.js +3 -1
  33. package/clis/twitter/bookmarks.js +3 -1
  34. package/clis/twitter/followers.js +20 -5
  35. package/clis/twitter/followers.test.js +44 -0
  36. package/clis/twitter/following.js +36 -20
  37. package/clis/twitter/following.test.js +60 -8
  38. package/clis/twitter/likes.js +28 -13
  39. package/clis/twitter/likes.test.js +111 -1
  40. package/clis/twitter/list-add.js +128 -204
  41. package/clis/twitter/list-add.test.js +97 -1
  42. package/clis/twitter/list-tweets.js +13 -4
  43. package/clis/twitter/list-tweets.test.js +48 -0
  44. package/clis/twitter/lists.js +5 -2
  45. package/clis/twitter/post.js +23 -4
  46. package/clis/twitter/post.test.js +30 -0
  47. package/clis/twitter/profile.js +16 -8
  48. package/clis/twitter/profile.test.js +39 -0
  49. package/clis/twitter/reply.js +133 -10
  50. package/clis/twitter/reply.test.js +55 -0
  51. package/clis/twitter/search.js +188 -170
  52. package/clis/twitter/search.test.js +96 -258
  53. package/clis/twitter/shared.js +167 -16
  54. package/clis/twitter/shared.test.js +102 -1
  55. package/clis/twitter/timeline.js +3 -1
  56. package/clis/twitter/tweets.js +147 -51
  57. package/clis/twitter/tweets.test.js +238 -1
  58. package/clis/xiaohongshu/comments.js +57 -26
  59. package/clis/xiaohongshu/comments.test.js +63 -1
  60. package/clis/xiaohongshu/download.js +32 -23
  61. package/clis/xiaohongshu/feed.js +23 -15
  62. package/clis/xiaohongshu/note-helpers.js +16 -6
  63. package/clis/xiaohongshu/note.js +26 -20
  64. package/clis/xiaohongshu/notifications.js +26 -19
  65. package/clis/xiaohongshu/search.js +201 -37
  66. package/clis/xiaohongshu/search.test.js +82 -8
  67. package/clis/xiaohongshu/user-helpers.js +13 -4
  68. package/clis/xiaohongshu/user-helpers.test.js +20 -0
  69. package/clis/xiaohongshu/user.js +9 -4
  70. package/clis/xueqiu/earnings-date.js +2 -2
  71. package/clis/xueqiu/kline.js +2 -2
  72. package/clis/xueqiu/utils.js +19 -0
  73. package/clis/xueqiu/utils.test.js +26 -0
  74. package/clis/youtube/transcript.js +28 -3
  75. package/clis/youtube/transcript.test.js +90 -1
  76. package/clis/zhihu/answer-detail.js +233 -0
  77. package/clis/zhihu/answer-detail.test.js +330 -0
  78. package/clis/zhihu/question.js +44 -10
  79. package/clis/zhihu/question.test.js +78 -1
  80. package/clis/zhihu/recommend.js +103 -0
  81. package/clis/zhihu/recommend.test.js +143 -0
  82. package/dist/src/browser/base-page.d.ts +3 -2
  83. package/dist/src/browser/base-page.test.js +2 -2
  84. package/dist/src/browser/cdp.js +3 -3
  85. package/dist/src/browser/page.d.ts +3 -2
  86. package/dist/src/browser/page.js +4 -4
  87. package/dist/src/browser/page.test.js +31 -0
  88. package/dist/src/browser/utils.d.ts +10 -0
  89. package/dist/src/browser/utils.js +37 -0
  90. package/dist/src/browser/utils.test.d.ts +1 -0
  91. package/dist/src/browser/utils.test.js +29 -0
  92. package/dist/src/cli-argv-preprocess.d.ts +37 -0
  93. package/dist/src/cli-argv-preprocess.js +131 -0
  94. package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
  95. package/dist/src/cli-argv-preprocess.test.js +130 -0
  96. package/dist/src/cli.js +123 -86
  97. package/dist/src/cli.test.js +32 -22
  98. package/dist/src/commands/daemon.js +6 -7
  99. package/dist/src/doctor.js +21 -17
  100. package/dist/src/doctor.test.js +2 -0
  101. package/dist/src/download/progress.js +15 -11
  102. package/dist/src/download/progress.test.d.ts +1 -0
  103. package/dist/src/download/progress.test.js +25 -0
  104. package/dist/src/execution.js +1 -3
  105. package/dist/src/execution.test.js +4 -16
  106. package/dist/src/help.d.ts +11 -0
  107. package/dist/src/help.js +46 -5
  108. package/dist/src/logger.js +8 -9
  109. package/dist/src/main.js +16 -0
  110. package/dist/src/output.js +4 -5
  111. package/dist/src/runtime-detect.d.ts +1 -1
  112. package/dist/src/runtime-detect.js +1 -1
  113. package/dist/src/runtime-detect.test.js +3 -2
  114. package/dist/src/tui.d.ts +0 -1
  115. package/dist/src/tui.js +9 -22
  116. package/dist/src/types.d.ts +3 -1
  117. package/dist/src/update-check.js +4 -5
  118. package/package.json +5 -4
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import { AuthRequiredError, CliError } from '@jackwener/opencli/errors';
4
+ import './recommend.js';
5
+
6
+ describe('zhihu recommend', () => {
7
+ it('returns recommendations from the Zhihu feed API', async () => {
8
+ const cmd = getRegistry().get('zhihu/recommend');
9
+ expect(cmd?.func).toBeTypeOf('function');
10
+ const goto = vi.fn().mockResolvedValue(undefined);
11
+ const evaluate = vi.fn().mockImplementation(async (js) => {
12
+ expect(js).toContain('/api/v3/feed/topstory/recommend?limit=10&desktop=true');
13
+ expect(js).toContain("credentials: 'include'");
14
+ return {
15
+ data: [
16
+ {
17
+ id: '0_1',
18
+ target: {
19
+ id: '101',
20
+ type: 'answer',
21
+ author: { name: 'alice' },
22
+ voteup_count: 12,
23
+ question: { id: '202', title: 'Question title' },
24
+ },
25
+ },
26
+ {
27
+ id: '1_1',
28
+ target: {
29
+ id: '303',
30
+ type: 'article',
31
+ title: 'Article title',
32
+ author: { name: 'bob' },
33
+ reaction: { statistics: { like_count: 7 } },
34
+ },
35
+ },
36
+ ],
37
+ paging: { is_end: true },
38
+ };
39
+ });
40
+ const page = { goto, evaluate };
41
+ await expect(cmd.func(page, { limit: 2 })).resolves.toEqual([
42
+ {
43
+ rank: 1,
44
+ type: 'answer',
45
+ title: 'Question title',
46
+ author: 'alice',
47
+ votes: 12,
48
+ url: 'https://www.zhihu.com/question/202/answer/101',
49
+ },
50
+ {
51
+ rank: 2,
52
+ type: 'article',
53
+ title: 'Article title',
54
+ author: 'bob',
55
+ votes: 7,
56
+ url: 'https://zhuanlan.zhihu.com/p/303',
57
+ },
58
+ ]);
59
+ expect(goto).toHaveBeenCalledWith('https://www.zhihu.com');
60
+ expect(evaluate).toHaveBeenCalledTimes(1);
61
+ });
62
+
63
+ it('follows paging.next until the requested limit is reached', async () => {
64
+ const cmd = getRegistry().get('zhihu/recommend');
65
+ const page = {
66
+ goto: vi.fn().mockResolvedValue(undefined),
67
+ evaluate: vi.fn()
68
+ .mockResolvedValueOnce({
69
+ data: [
70
+ { id: '0_1', target: { id: 'a1', type: 'answer', author: { name: 'alice' }, question: { id: 'q1', title: 'first' } } },
71
+ { id: '1_1', target: { id: 'a2', type: 'answer', author: { name: 'bob' }, question: { id: 'q2', title: 'second' } } },
72
+ ],
73
+ paging: {
74
+ is_end: false,
75
+ next: 'https://www.zhihu.com/api/v3/feed/topstory/recommend?action=down&after_id=1&page_number=2',
76
+ },
77
+ })
78
+ .mockResolvedValueOnce({
79
+ data: [
80
+ { id: '1_1', target: { id: 'a2', type: 'answer', author: { name: 'bob duplicate' }, question: { id: 'q2', title: 'duplicate' } } },
81
+ { id: '2_1', target: { id: 'q3', type: 'question', title: 'third' } },
82
+ ],
83
+ paging: { is_end: true },
84
+ }),
85
+ };
86
+ await expect(cmd.func(page, { limit: 3 })).resolves.toEqual([
87
+ { rank: 1, type: 'answer', title: 'first', author: 'alice', votes: 0, url: 'https://www.zhihu.com/question/q1/answer/a1' },
88
+ { rank: 2, type: 'answer', title: 'second', author: 'bob', votes: 0, url: 'https://www.zhihu.com/question/q2/answer/a2' },
89
+ { rank: 3, type: 'question', title: 'third', author: '', votes: 0, url: 'https://www.zhihu.com/question/q3' },
90
+ ]);
91
+ expect(page.evaluate).toHaveBeenCalledTimes(2);
92
+ expect(page.evaluate.mock.calls[1][0]).toContain('after_id=1');
93
+ });
94
+
95
+ it('maps auth-like failures to AuthRequiredError', async () => {
96
+ const cmd = getRegistry().get('zhihu/recommend');
97
+ const page = {
98
+ goto: vi.fn().mockResolvedValue(undefined),
99
+ evaluate: vi.fn().mockResolvedValue({ __httpError: 403 }),
100
+ };
101
+ await expect(cmd.func(page, { limit: 3 })).rejects.toBeInstanceOf(AuthRequiredError);
102
+ });
103
+
104
+ it('preserves non-auth fetch failures as CliError', async () => {
105
+ const cmd = getRegistry().get('zhihu/recommend');
106
+ const page = {
107
+ goto: vi.fn().mockResolvedValue(undefined),
108
+ evaluate: vi.fn().mockResolvedValue({ __httpError: 500 }),
109
+ };
110
+ await expect(cmd.func(page, { limit: 3 })).rejects.toMatchObject({
111
+ code: 'FETCH_ERROR',
112
+ message: 'Zhihu recommendations request failed (HTTP 500)',
113
+ });
114
+ });
115
+
116
+ it('handles null evaluate response as fetch error', async () => {
117
+ const cmd = getRegistry().get('zhihu/recommend');
118
+ const page = {
119
+ goto: vi.fn().mockResolvedValue(undefined),
120
+ evaluate: vi.fn().mockResolvedValue(null),
121
+ };
122
+ await expect(cmd.func(page, { limit: 3 })).rejects.toMatchObject({
123
+ code: 'FETCH_ERROR',
124
+ message: 'Zhihu recommendations request failed',
125
+ });
126
+ });
127
+
128
+ it('rejects invalid limits before navigation', async () => {
129
+ const cmd = getRegistry().get('zhihu/recommend');
130
+ const page = { goto: vi.fn(), evaluate: vi.fn() };
131
+ await expect(cmd.func(page, { limit: 0 })).rejects.toBeInstanceOf(CliError);
132
+ expect(page.goto).not.toHaveBeenCalled();
133
+ expect(page.evaluate).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it('rejects excessive limits before navigation', async () => {
137
+ const cmd = getRegistry().get('zhihu/recommend');
138
+ const page = { goto: vi.fn(), evaluate: vi.fn() };
139
+ await expect(cmd.func(page, { limit: 1001 })).rejects.toBeInstanceOf(CliError);
140
+ expect(page.goto).not.toHaveBeenCalled();
141
+ expect(page.evaluate).not.toHaveBeenCalled();
142
+ });
143
+ });
@@ -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, FetchJsonOptions, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
11
+ import type { BrowserCookie, BrowserEvaluateFunction, 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;
@@ -60,7 +60,8 @@ export declare abstract class BasePage implements IPage {
60
60
  settleMs?: number;
61
61
  allowBoundNavigation?: boolean;
62
62
  }): Promise<void>;
63
- abstract evaluate(js: string): Promise<unknown>;
63
+ abstract evaluate<T = unknown>(js: string): Promise<T>;
64
+ abstract evaluate<Args extends unknown[], T>(fn: BrowserEvaluateFunction<Args, T>, ...args: Args): Promise<Awaited<T>>;
64
65
  /**
65
66
  * Safely evaluate JS with pre-serialized arguments.
66
67
  * Each key in `args` becomes a `const` declaration with JSON-serialized value,
@@ -29,8 +29,8 @@ class ActionPage extends BasePage {
29
29
  setFileInput;
30
30
  cdp;
31
31
  async goto() { }
32
- async evaluate(js) {
33
- this.scripts.push(js);
32
+ async evaluate(input) {
33
+ this.scripts.push(typeof input === 'string' ? input : input.toString());
34
34
  return this.results.shift() ?? null;
35
35
  }
36
36
  async evaluateWithArgs(js, args) {
@@ -10,7 +10,7 @@
10
10
  import { WebSocket } from 'ws';
11
11
  import { request as httpRequest } from 'node:http';
12
12
  import { request as httpsRequest } from 'node:https';
13
- import { wrapForEval } from './utils.js';
13
+ import { buildEvaluateExpression } from './utils.js';
14
14
  import { generateStealthJs } from './stealth.js';
15
15
  import { waitForDomStableJs } from './dom-helpers.js';
16
16
  import { isRecord, saveBase64ToFile } from '../utils.js';
@@ -181,8 +181,8 @@ class CDPPage extends BasePage {
181
181
  await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
182
182
  }
183
183
  }
184
- async evaluate(js) {
185
- const expression = wrapForEval(js);
184
+ async evaluate(input, ...args) {
185
+ const expression = buildEvaluateExpression(input, args);
186
186
  const result = await this.bridge.send('Runtime.evaluate', {
187
187
  expression,
188
188
  returnByValue: true,
@@ -8,7 +8,7 @@
8
8
  * by the navigate action and pass it to all subsequent commands. This ensures
9
9
  * page-scoped operations target the correct page without guessing.
10
10
  */
11
- import type { BrowserCookie, BrowserDownloadWaitResult, ScreenshotOptions } from '../types.js';
11
+ import type { BrowserCookie, BrowserDownloadWaitResult, BrowserEvaluateFunction, ScreenshotOptions } from '../types.js';
12
12
  import { BasePage } from './base-page.js';
13
13
  /**
14
14
  * Page — implements IPage by talking to the daemon via HTTP.
@@ -38,7 +38,8 @@ export declare class Page extends BasePage {
38
38
  /** Bind this Page instance to a specific page identity (targetId). */
39
39
  setActivePage(page?: string): void;
40
40
  private _markUnsupportedNetworkCapture;
41
- evaluate(js: string): Promise<unknown>;
41
+ evaluate<T = unknown>(js: string): Promise<T>;
42
+ evaluate<Args extends unknown[], T>(fn: BrowserEvaluateFunction<Args, T>, ...args: Args): Promise<Awaited<T>>;
42
43
  getCookies(opts?: {
43
44
  domain?: string;
44
45
  url?: string;
@@ -9,7 +9,7 @@
9
9
  * page-scoped operations target the correct page without guessing.
10
10
  */
11
11
  import { sendCommand, sendCommandFull } from './daemon-client.js';
12
- import { wrapForEval } from './utils.js';
12
+ import { buildEvaluateExpression } from './utils.js';
13
13
  import { saveBase64ToFile } from '../utils.js';
14
14
  import { generateStealthJs } from './stealth.js';
15
15
  import { waitForDomStableJs } from './dom-helpers.js';
@@ -137,8 +137,8 @@ export class Page extends BasePage {
137
137
  log.warn('Browser Bridge extension does not support network capture; continuing without it. ' +
138
138
  'Explore output may miss API endpoints until you reload or reinstall the extension.');
139
139
  }
140
- async evaluate(js) {
141
- const code = wrapForEval(js);
140
+ async evaluate(input, ...args) {
141
+ const code = buildEvaluateExpression(input, args);
142
142
  try {
143
143
  return await sendCommand('exec', { code, ...this._cmdOpts() });
144
144
  }
@@ -294,7 +294,7 @@ export class Page extends BasePage {
294
294
  return Array.isArray(result) ? result : [];
295
295
  }
296
296
  async evaluateInFrame(js, frameIndex) {
297
- const code = wrapForEval(js);
297
+ const code = buildEvaluateExpression(js);
298
298
  return sendCommand('exec', { code, frameIndex, ...this._cmdOpts() });
299
299
  }
300
300
  async cdp(method, params = {}) {
@@ -74,6 +74,37 @@ describe('Page.evaluate', () => {
74
74
  expect(value).toBe(42);
75
75
  expect(sendCommandMock).toHaveBeenCalledTimes(2);
76
76
  });
77
+ it('serializes function-form evaluate calls with JSON args', async () => {
78
+ sendCommandMock.mockResolvedValueOnce('/opencli');
79
+ const page = new Page('twitter', undefined, undefined, undefined, 'adapter');
80
+ const href = await page.evaluate((selector) => {
81
+ const link = document.querySelector(selector);
82
+ return link ? link.getAttribute('href') : null;
83
+ }, 'a[data-testid="AppTabBar_Profile_Link"]');
84
+ expect(href).toBe('/opencli');
85
+ expect(sendCommandMock).toHaveBeenCalledWith('exec', expect.objectContaining({
86
+ session: 'twitter',
87
+ surface: 'adapter',
88
+ code: expect.stringContaining('(...["a[data-testid=\\"AppTabBar_Profile_Link\\"]"])'),
89
+ }));
90
+ const code = sendCommandMock.mock.calls[0]?.[1]?.code;
91
+ expect(code).toContain('document.querySelector(selector)');
92
+ });
93
+ it('rejects non-JSON-serializable evaluate args before sending to the daemon', async () => {
94
+ const page = new Page('default');
95
+ const circular = {};
96
+ circular.self = circular;
97
+ await expect(page.evaluate((value) => value, circular)).rejects.toThrow('JSON-serializable');
98
+ expect(sendCommandMock).not.toHaveBeenCalled();
99
+ });
100
+ it('keeps string evaluate behavior unchanged', async () => {
101
+ sendCommandMock.mockResolvedValueOnce(42);
102
+ const page = new Page('default');
103
+ await expect(page.evaluate('21 + 21')).resolves.toBe(42);
104
+ expect(sendCommandMock).toHaveBeenCalledWith('exec', expect.objectContaining({
105
+ code: '21 + 21',
106
+ }));
107
+ });
77
108
  });
78
109
  describe('Page network capture compatibility', () => {
79
110
  beforeEach(() => {
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * Utility functions for browser operations
3
3
  */
4
+ type EvaluateFunction = (...args: any[]) => unknown;
5
+ /**
6
+ * Serialize a function-form page.evaluate call for CDP Runtime.evaluate.
7
+ *
8
+ * Functions execute in the browser page context, so they cannot close over
9
+ * Node-side variables. Pass external values as JSON-serializable args instead.
10
+ */
11
+ export declare function serializeFunctionForEval(fn: EvaluateFunction, args?: readonly unknown[]): string;
4
12
  /**
5
13
  * Wrap JS code for CDP Runtime.evaluate:
6
14
  * - Already an IIFE `(...)()` → send as-is
@@ -8,3 +16,5 @@
8
16
  * - `new Promise(...)` or raw expression → send as-is (expression)
9
17
  */
10
18
  export declare function wrapForEval(js: string): string;
19
+ export declare function buildEvaluateExpression(input: string | EvaluateFunction, args?: readonly unknown[]): string;
20
+ export {};
@@ -1,6 +1,34 @@
1
1
  /**
2
2
  * Utility functions for browser operations
3
3
  */
4
+ function describeJsonError(err) {
5
+ return err instanceof Error ? err.message : String(err);
6
+ }
7
+ /**
8
+ * Serialize a function-form page.evaluate call for CDP Runtime.evaluate.
9
+ *
10
+ * Functions execute in the browser page context, so they cannot close over
11
+ * Node-side variables. Pass external values as JSON-serializable args instead.
12
+ */
13
+ export function serializeFunctionForEval(fn, args = []) {
14
+ const source = fn.toString().trim();
15
+ const isFunctionSource = /^(async\s+)?function[\s(]/.test(source)
16
+ || /^(async\s*)?(\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(source);
17
+ if (!isFunctionSource || source.includes('[native code]')) {
18
+ throw new Error('page.evaluate(fn) requires a serializable arrow/function expression');
19
+ }
20
+ let serializedArgs;
21
+ try {
22
+ serializedArgs = JSON.stringify(args);
23
+ }
24
+ catch (err) {
25
+ throw new Error(`page.evaluate arguments must be JSON-serializable: ${describeJsonError(err)}`);
26
+ }
27
+ if (serializedArgs === undefined) {
28
+ throw new Error('page.evaluate arguments must be JSON-serializable');
29
+ }
30
+ return `(${source})(...${serializedArgs})`;
31
+ }
4
32
  /**
5
33
  * Wrap JS code for CDP Runtime.evaluate:
6
34
  * - Already an IIFE `(...)()` → send as-is
@@ -25,3 +53,12 @@ export function wrapForEval(js) {
25
53
  // Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
26
54
  return code;
27
55
  }
56
+ export function buildEvaluateExpression(input, args = []) {
57
+ if (typeof input === 'function') {
58
+ return serializeFunctionForEval(input, args);
59
+ }
60
+ if (args.length > 0) {
61
+ throw new Error('page.evaluate string input does not accept args; use page.evaluate(fn, ...args) instead');
62
+ }
63
+ return wrapForEval(input);
64
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildEvaluateExpression, wrapForEval } from './utils.js';
3
+ describe('browser eval utils', () => {
4
+ it('keeps existing string eval wrapping behavior', () => {
5
+ expect(wrapForEval('21 + 21')).toBe('21 + 21');
6
+ expect(wrapForEval('() => 42')).toBe('(() => 42)()');
7
+ });
8
+ it('serializes function eval arguments as JSON', () => {
9
+ const code = buildEvaluateExpression((selector) => {
10
+ return document.querySelector(selector)?.textContent ?? null;
11
+ }, ['.title']);
12
+ expect(code).toContain('document.querySelector(selector)');
13
+ expect(code).toContain('(...[".title"])');
14
+ });
15
+ it('accepts compact async arrow functions', () => {
16
+ const fn = new Function('return async()=>42')();
17
+ expect(buildEvaluateExpression(fn)).toBe('(async()=>42)(...[])');
18
+ });
19
+ it('rejects string eval with stray args', () => {
20
+ expect(() => buildEvaluateExpression('document.title', ['ignored']))
21
+ .toThrow('use page.evaluate(fn, ...args)');
22
+ });
23
+ it('rejects non-JSON-serializable function args', () => {
24
+ const circular = {};
25
+ circular.self = circular;
26
+ expect(() => buildEvaluateExpression((value) => value, [circular]))
27
+ .toThrow('JSON-serializable');
28
+ });
29
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * argv preprocessing: rewrite `opencli browser <session> <subcommand> ...`
3
+ * into `opencli browser --session <session> <subcommand> ...` so commander
4
+ * (which can't combine a parent positional with subcommand dispatch) can parse it.
5
+ *
6
+ * The user-facing form is positional; the internal form uses --session. Help text
7
+ * for the `browser` command is overridden to advertise the positional form.
8
+ */
9
+ /**
10
+ * Returns the set of reserved subcommand names (exposed for tests so they stay
11
+ * synced with the actual registrations in cli.ts).
12
+ */
13
+ export declare function getBrowserSubcommandNames(): ReadonlySet<string>;
14
+ /**
15
+ * Rewrite `argv` to convert the positional `<session>` after `browser`
16
+ * into the internal `--session <name>` flag form.
17
+ *
18
+ * Only acts when `browser` is the root command (i.e. the first non-flag token
19
+ * after any leading root options), so it can't mis-interpret occurrences of
20
+ * the literal word `browser` deeper in the argv (e.g. `opencli adapter init
21
+ * browser/x`, or a URL value containing `browser`).
22
+ *
23
+ * Leaves argv unchanged when:
24
+ * - root command is not `browser`
25
+ * - the token after `browser` is a flag (e.g. `--help`)
26
+ * - the token after `browser` is a known subcommand name (session was
27
+ * omitted; commander will surface its own required-flag error)
28
+ */
29
+ export declare function rewriteBrowserArgv(argv: readonly string[]): string[];
30
+ /**
31
+ * Thrown by the preprocessor when user argv uses a retired/old form that we
32
+ * intentionally refuse to accept. main.ts catches this and exits with a
33
+ * usage error so it does not bubble up as an internal stacktrace.
34
+ */
35
+ export declare class BrowserSessionArgvError extends Error {
36
+ constructor(message: string);
37
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * argv preprocessing: rewrite `opencli browser <session> <subcommand> ...`
3
+ * into `opencli browser --session <session> <subcommand> ...` so commander
4
+ * (which can't combine a parent positional with subcommand dispatch) can parse it.
5
+ *
6
+ * The user-facing form is positional; the internal form uses --session. Help text
7
+ * for the `browser` command is overridden to advertise the positional form.
8
+ */
9
+ /**
10
+ * Browser subcommand names. If `<session>` would collide with one of these,
11
+ * we treat it as a missing-positional error and leave argv alone so commander
12
+ * reports a usable diagnostic.
13
+ *
14
+ * Keep in sync with the subcommands declared on the `browser` command in cli.ts.
15
+ */
16
+ const BROWSER_SUBCOMMAND_NAMES = new Set([
17
+ 'analyze',
18
+ 'back',
19
+ 'bind',
20
+ 'check',
21
+ 'click',
22
+ 'close',
23
+ 'console',
24
+ 'dblclick',
25
+ 'dialog',
26
+ 'drag',
27
+ 'eval',
28
+ 'extract',
29
+ 'fill',
30
+ 'find',
31
+ 'focus',
32
+ 'frames',
33
+ 'get',
34
+ 'help',
35
+ 'hover',
36
+ 'init',
37
+ 'keys',
38
+ 'network',
39
+ 'open',
40
+ 'screenshot',
41
+ 'scroll',
42
+ 'select',
43
+ 'state',
44
+ 'tab',
45
+ 'type',
46
+ 'unbind',
47
+ 'uncheck',
48
+ 'upload',
49
+ 'verify',
50
+ 'wait',
51
+ ]);
52
+ /**
53
+ * Root program options that consume the following token as their value. Used by
54
+ * the preprocessor to identify which token is the root command name (so e.g.
55
+ * `opencli --profile work browser foo state` is recognised as the `browser`
56
+ * command with `<session>=foo`, not the value of --profile).
57
+ *
58
+ * Keep in sync with `program.option(...)` calls in cli.ts.
59
+ */
60
+ const ROOT_VALUE_FLAGS = new Set(['--profile']);
61
+ /**
62
+ * Returns the set of reserved subcommand names (exposed for tests so they stay
63
+ * synced with the actual registrations in cli.ts).
64
+ */
65
+ export function getBrowserSubcommandNames() {
66
+ return BROWSER_SUBCOMMAND_NAMES;
67
+ }
68
+ /**
69
+ * Rewrite `argv` to convert the positional `<session>` after `browser`
70
+ * into the internal `--session <name>` flag form.
71
+ *
72
+ * Only acts when `browser` is the root command (i.e. the first non-flag token
73
+ * after any leading root options), so it can't mis-interpret occurrences of
74
+ * the literal word `browser` deeper in the argv (e.g. `opencli adapter init
75
+ * browser/x`, or a URL value containing `browser`).
76
+ *
77
+ * Leaves argv unchanged when:
78
+ * - root command is not `browser`
79
+ * - the token after `browser` is a flag (e.g. `--help`)
80
+ * - the token after `browser` is a known subcommand name (session was
81
+ * omitted; commander will surface its own required-flag error)
82
+ */
83
+ export function rewriteBrowserArgv(argv) {
84
+ const result = [...argv];
85
+ // Walk past leading root flags + their values to find the root command token.
86
+ let i = 0;
87
+ while (i < result.length) {
88
+ const tok = result[i];
89
+ if (!tok.startsWith('-'))
90
+ break;
91
+ // `--flag=value` consumes one slot regardless of whether the flag expects a value.
92
+ if (tok.includes('=')) {
93
+ i += 1;
94
+ continue;
95
+ }
96
+ if (ROOT_VALUE_FLAGS.has(tok) && i + 1 < result.length) {
97
+ i += 2;
98
+ }
99
+ else {
100
+ i += 1;
101
+ }
102
+ }
103
+ if (result[i] !== 'browser')
104
+ return result;
105
+ const sessionIdx = i + 1;
106
+ const next = result[sessionIdx];
107
+ if (next === undefined)
108
+ return result;
109
+ // The retired `--session` flag must not be a working public entrance.
110
+ if (next === '--session' || next === '--session=' || next.startsWith('--session=')) {
111
+ throw new BrowserSessionArgvError('The `--session` flag is no longer a public option. Use the positional form: opencli browser <session> <command>');
112
+ }
113
+ if (next.startsWith('-'))
114
+ return result;
115
+ if (BROWSER_SUBCOMMAND_NAMES.has(next))
116
+ return result;
117
+ // Splice in --session <name> in place of the positional.
118
+ result.splice(sessionIdx, 1, '--session', next);
119
+ return result;
120
+ }
121
+ /**
122
+ * Thrown by the preprocessor when user argv uses a retired/old form that we
123
+ * intentionally refuse to accept. main.ts catches this and exits with a
124
+ * usage error so it does not bubble up as an internal stacktrace.
125
+ */
126
+ export class BrowserSessionArgvError extends Error {
127
+ constructor(message) {
128
+ super(message);
129
+ this.name = 'BrowserSessionArgvError';
130
+ }
131
+ }
@@ -0,0 +1 @@
1
+ export {};