@jackwener/opencli 1.7.4 → 1.7.6

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 (181) hide show
  1. package/README.md +76 -51
  2. package/README.zh-CN.md +78 -62
  3. package/cli-manifest.json +4558 -2979
  4. package/clis/antigravity/serve.js +71 -25
  5. package/clis/baidu-scholar/search.js +87 -0
  6. package/clis/baidu-scholar/search.test.js +23 -0
  7. package/clis/bilibili/video.js +61 -0
  8. package/clis/bilibili/video.test.js +81 -0
  9. package/clis/deepseek/ask.js +94 -0
  10. package/clis/deepseek/ask.test.js +73 -0
  11. package/clis/deepseek/history.js +25 -0
  12. package/clis/deepseek/new.js +20 -0
  13. package/clis/deepseek/read.js +22 -0
  14. package/clis/deepseek/status.js +24 -0
  15. package/clis/deepseek/utils.js +291 -0
  16. package/clis/deepseek/utils.test.js +37 -0
  17. package/clis/eastmoney/_secid.js +78 -0
  18. package/clis/eastmoney/announcement.js +52 -0
  19. package/clis/eastmoney/convertible.js +73 -0
  20. package/clis/eastmoney/etf.js +65 -0
  21. package/clis/eastmoney/holders.js +78 -0
  22. package/clis/eastmoney/index-board.js +96 -0
  23. package/clis/eastmoney/kline.js +87 -0
  24. package/clis/eastmoney/kuaixun.js +54 -0
  25. package/clis/eastmoney/longhu.js +67 -0
  26. package/clis/eastmoney/money-flow.js +78 -0
  27. package/clis/eastmoney/northbound.js +57 -0
  28. package/clis/eastmoney/quote.js +107 -0
  29. package/clis/eastmoney/rank.js +94 -0
  30. package/clis/eastmoney/sectors.js +76 -0
  31. package/clis/google-scholar/search.js +58 -0
  32. package/clis/google-scholar/search.test.js +23 -0
  33. package/clis/gov-law/commands.test.js +39 -0
  34. package/clis/gov-law/recent.js +22 -0
  35. package/clis/gov-law/search.js +41 -0
  36. package/clis/gov-law/shared.js +51 -0
  37. package/clis/gov-policy/commands.test.js +27 -0
  38. package/clis/gov-policy/recent.js +47 -0
  39. package/clis/gov-policy/search.js +48 -0
  40. package/clis/jianyu/search.js +139 -3
  41. package/clis/jianyu/search.test.js +25 -0
  42. package/clis/jianyu/shared/procurement-detail.js +15 -0
  43. package/clis/jianyu/shared/procurement-detail.test.js +12 -0
  44. package/clis/nowcoder/companies.js +23 -0
  45. package/clis/nowcoder/creators.js +27 -0
  46. package/clis/nowcoder/detail.js +61 -0
  47. package/clis/nowcoder/experience.js +36 -0
  48. package/clis/nowcoder/hot.js +24 -0
  49. package/clis/nowcoder/jobs.js +21 -0
  50. package/clis/nowcoder/notifications.js +29 -0
  51. package/clis/nowcoder/papers.js +40 -0
  52. package/clis/nowcoder/practice.js +37 -0
  53. package/clis/nowcoder/recommend.js +30 -0
  54. package/clis/nowcoder/referral.js +39 -0
  55. package/clis/nowcoder/salary.js +40 -0
  56. package/clis/nowcoder/search.js +49 -0
  57. package/clis/nowcoder/suggest.js +33 -0
  58. package/clis/nowcoder/topics.js +27 -0
  59. package/clis/nowcoder/trending.js +25 -0
  60. package/clis/twitter/list-add.js +337 -0
  61. package/clis/twitter/list-add.test.js +15 -0
  62. package/clis/twitter/list-remove.js +297 -0
  63. package/clis/twitter/list-remove.test.js +14 -0
  64. package/clis/twitter/list-tweets.js +185 -0
  65. package/clis/twitter/list-tweets.test.js +108 -0
  66. package/clis/twitter/lists.js +134 -47
  67. package/clis/twitter/lists.test.js +105 -38
  68. package/clis/twitter/shared.js +7 -2
  69. package/clis/twitter/tweets.js +218 -0
  70. package/clis/twitter/tweets.test.js +125 -0
  71. package/clis/wanfang/search.js +66 -0
  72. package/clis/wanfang/search.test.js +23 -0
  73. package/clis/web/read.js +1 -1
  74. package/clis/weixin/download.js +3 -2
  75. package/clis/xiaohongshu/publish.js +149 -28
  76. package/clis/xiaohongshu/publish.test.js +319 -6
  77. package/clis/xiaoyuzhou/download.js +8 -4
  78. package/clis/xiaoyuzhou/download.test.js +23 -13
  79. package/clis/xiaoyuzhou/episode.js +9 -4
  80. package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
  81. package/clis/xiaoyuzhou/podcast.js +9 -4
  82. package/clis/xiaoyuzhou/utils.js +0 -40
  83. package/clis/xiaoyuzhou/utils.test.js +15 -75
  84. package/clis/youtube/channel.js +35 -0
  85. package/clis/zsxq/dynamics.js +1 -1
  86. package/clis/zsxq/utils.js +6 -3
  87. package/clis/zsxq/utils.test.js +31 -0
  88. package/dist/src/browser/base-page.d.ts +14 -4
  89. package/dist/src/browser/base-page.js +35 -25
  90. package/dist/src/browser/bridge.d.ts +1 -0
  91. package/dist/src/browser/bridge.js +1 -1
  92. package/dist/src/browser/cdp.d.ts +1 -0
  93. package/dist/src/browser/cdp.js +13 -4
  94. package/dist/src/browser/compound.d.ts +59 -0
  95. package/dist/src/browser/compound.js +112 -0
  96. package/dist/src/browser/compound.test.js +175 -0
  97. package/dist/src/browser/daemon-client.d.ts +6 -4
  98. package/dist/src/browser/daemon-client.js +6 -1
  99. package/dist/src/browser/daemon-client.test.js +40 -1
  100. package/dist/src/browser/dom-snapshot.d.ts +7 -0
  101. package/dist/src/browser/dom-snapshot.js +83 -5
  102. package/dist/src/browser/dom-snapshot.test.js +65 -0
  103. package/dist/src/browser/extract.d.ts +69 -0
  104. package/dist/src/browser/extract.js +132 -0
  105. package/dist/src/browser/extract.test.js +129 -0
  106. package/dist/src/browser/find.d.ts +76 -0
  107. package/dist/src/browser/find.js +179 -0
  108. package/dist/src/browser/find.test.js +120 -0
  109. package/dist/src/browser/html-tree.d.ts +75 -0
  110. package/dist/src/browser/html-tree.js +112 -0
  111. package/dist/src/browser/html-tree.test.d.ts +1 -0
  112. package/dist/src/browser/html-tree.test.js +181 -0
  113. package/dist/src/browser/network-cache.d.ts +48 -0
  114. package/dist/src/browser/network-cache.js +66 -0
  115. package/dist/src/browser/network-cache.test.d.ts +1 -0
  116. package/dist/src/browser/network-cache.test.js +58 -0
  117. package/dist/src/browser/network-key.d.ts +22 -0
  118. package/dist/src/browser/network-key.js +66 -0
  119. package/dist/src/browser/network-key.test.d.ts +1 -0
  120. package/dist/src/browser/network-key.test.js +49 -0
  121. package/dist/src/browser/page.d.ts +14 -4
  122. package/dist/src/browser/page.js +48 -7
  123. package/dist/src/browser/page.test.js +97 -0
  124. package/dist/src/browser/shape-filter.d.ts +52 -0
  125. package/dist/src/browser/shape-filter.js +101 -0
  126. package/dist/src/browser/shape-filter.test.d.ts +1 -0
  127. package/dist/src/browser/shape-filter.test.js +101 -0
  128. package/dist/src/browser/shape.d.ts +23 -0
  129. package/dist/src/browser/shape.js +95 -0
  130. package/dist/src/browser/shape.test.d.ts +1 -0
  131. package/dist/src/browser/shape.test.js +82 -0
  132. package/dist/src/browser/target-errors.d.ts +14 -1
  133. package/dist/src/browser/target-errors.js +13 -0
  134. package/dist/src/browser/target-errors.test.js +39 -6
  135. package/dist/src/browser/target-resolver.d.ts +57 -10
  136. package/dist/src/browser/target-resolver.js +195 -75
  137. package/dist/src/browser/target-resolver.test.js +80 -5
  138. package/dist/src/cli.js +849 -267
  139. package/dist/src/cli.test.js +961 -90
  140. package/dist/src/commanderAdapter.d.ts +0 -1
  141. package/dist/src/commanderAdapter.js +2 -16
  142. package/dist/src/commanderAdapter.test.js +1 -1
  143. package/dist/src/completion-shared.js +2 -5
  144. package/dist/src/daemon.js +8 -0
  145. package/dist/src/download/article-download.d.ts +1 -0
  146. package/dist/src/download/article-download.js +3 -0
  147. package/dist/src/download/article-download.test.d.ts +1 -0
  148. package/dist/src/download/article-download.test.js +39 -0
  149. package/dist/src/execution.js +7 -2
  150. package/dist/src/execution.test.js +54 -0
  151. package/dist/src/main.js +16 -0
  152. package/dist/src/plugin.d.ts +1 -8
  153. package/dist/src/plugin.js +1 -27
  154. package/dist/src/plugin.test.js +1 -59
  155. package/dist/src/registry.d.ts +1 -0
  156. package/dist/src/registry.js +3 -2
  157. package/dist/src/registry.test.js +22 -0
  158. package/dist/src/types.d.ts +32 -8
  159. package/package.json +1 -1
  160. package/clis/twitter/lists-parser.js +0 -77
  161. package/clis/twitter/lists.d.ts +0 -5
  162. package/dist/src/cascade.d.ts +0 -46
  163. package/dist/src/cascade.js +0 -135
  164. package/dist/src/explore.d.ts +0 -99
  165. package/dist/src/explore.js +0 -402
  166. package/dist/src/generate-verified.d.ts +0 -105
  167. package/dist/src/generate-verified.js +0 -696
  168. package/dist/src/generate-verified.test.js +0 -925
  169. package/dist/src/generate.d.ts +0 -46
  170. package/dist/src/generate.js +0 -117
  171. package/dist/src/record.d.ts +0 -96
  172. package/dist/src/record.js +0 -657
  173. package/dist/src/record.test.js +0 -293
  174. package/dist/src/skill-generate.d.ts +0 -30
  175. package/dist/src/skill-generate.js +0 -75
  176. package/dist/src/skill-generate.test.js +0 -173
  177. package/dist/src/synthesize.d.ts +0 -97
  178. package/dist/src/synthesize.js +0 -208
  179. /package/dist/src/{generate-verified.test.d.ts → browser/compound.test.d.ts} +0 -0
  180. /package/dist/src/{record.test.d.ts → browser/extract.test.d.ts} +0 -0
  181. /package/dist/src/{skill-generate.test.d.ts → browser/find.test.d.ts} +0 -0
@@ -11,7 +11,6 @@
11
11
  */
12
12
  import { Command } from 'commander';
13
13
  import { type CliCommand } from './registry.js';
14
- export declare function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown;
15
14
  /**
16
15
  * Register a single CliCommand as a Commander subcommand.
17
16
  */
@@ -15,22 +15,8 @@ import { fullName, getRegistry } from './registry.js';
15
15
  import { formatRegistryHelpText } from './serialization.js';
16
16
  import { render as renderOutput } from './output.js';
17
17
  import { executeCommand, prepareCommandArgs } from './execution.js';
18
- import { CliError, EXIT_CODES, ArgumentError, toEnvelope, } from './errors.js';
18
+ import { CliError, EXIT_CODES, toEnvelope, } from './errors.js';
19
19
  import { isDiagnosticEnabled } from './diagnostic.js';
20
- export function normalizeArgValue(argType, value, name) {
21
- if (argType !== 'bool' && argType !== 'boolean')
22
- return value;
23
- if (typeof value === 'boolean')
24
- return value;
25
- if (value == null || value === '')
26
- return false;
27
- const normalized = String(value).trim().toLowerCase();
28
- if (normalized === 'true')
29
- return true;
30
- if (normalized === 'false')
31
- return false;
32
- throw new ArgumentError(`"${name}" must be either "true" or "false".`);
33
- }
34
20
  /**
35
21
  * Register a single CliCommand as a Commander subcommand.
36
22
  */
@@ -83,7 +69,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
83
69
  const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
84
70
  const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
85
71
  if (v !== undefined)
86
- rawKwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
72
+ rawKwargs[arg.name] = v;
87
73
  }
88
74
  const kwargs = prepareCommandArgs(cmd, rawKwargs);
89
75
  const verbose = optionsRecord.verbose === true;
@@ -61,7 +61,7 @@ describe('commanderAdapter arg passing', () => {
61
61
  const siteCmd = program.command('paperreview');
62
62
  registerCommandToProgram(siteCmd, cmd);
63
63
  await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
64
- // normalizeArgValue validates bools eagerly; executeCommand should not be reached
64
+ // prepareCommandArgs validates bools before dispatch; executeCommand should not be reached
65
65
  expect(mockExecuteCommand).not.toHaveBeenCalled();
66
66
  });
67
67
  });
@@ -11,11 +11,8 @@ export const BUILTIN_COMMANDS = [
11
11
  'list',
12
12
  'validate',
13
13
  'verify',
14
- 'explore',
15
- 'probe', // alias for explore
16
- 'synthesize',
17
- 'generate',
18
- 'cascade',
14
+ 'browser',
15
+ 'tab',
19
16
  'doctor',
20
17
  'plugin',
21
18
  'install',
@@ -154,6 +154,14 @@ async function handleRequest(req, res) {
154
154
  const timeoutMs = typeof body.timeout === 'number' && body.timeout > 0
155
155
  ? body.timeout * 1000
156
156
  : 120000;
157
+ if (pending.has(body.id)) {
158
+ jsonResponse(res, 409, {
159
+ id: body.id,
160
+ ok: false,
161
+ error: 'Duplicate command id already pending; retry',
162
+ });
163
+ return;
164
+ }
157
165
  const result = await new Promise((resolve, reject) => {
158
166
  const timer = setTimeout(() => {
159
167
  pending.delete(body.id);
@@ -44,6 +44,7 @@ export interface ArticleDownloadResult {
44
44
  publish_time: string;
45
45
  status: string;
46
46
  size: string;
47
+ saved: string;
47
48
  }
48
49
  /**
49
50
  * Download an article to Markdown with optional image localization.
@@ -129,6 +129,7 @@ export async function downloadArticle(data, options) {
129
129
  publish_time: '-',
130
130
  status: 'failed — no title',
131
131
  size: '-',
132
+ saved: '-',
132
133
  }];
133
134
  }
134
135
  if (!data.contentHtml) {
@@ -138,6 +139,7 @@ export async function downloadArticle(data, options) {
138
139
  publish_time: data.publishTime || '-',
139
140
  status: 'failed — no content',
140
141
  size: '-',
142
+ saved: '-',
141
143
  }];
142
144
  }
143
145
  // Convert HTML to Markdown
@@ -174,5 +176,6 @@ export async function downloadArticle(data, options) {
174
176
  publish_time: data.publishTime || '-',
175
177
  status: 'success',
176
178
  size: formatBytes(size),
179
+ saved: filePath,
177
180
  }];
178
181
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { downloadArticle } from './article-download.js';
6
+ const tempDirs = [];
7
+ afterEach(() => {
8
+ for (const dir of tempDirs) {
9
+ try {
10
+ fs.rmSync(dir, { recursive: true, force: true });
11
+ }
12
+ catch {
13
+ // Ignore cleanup errors in tests.
14
+ }
15
+ }
16
+ tempDirs.length = 0;
17
+ });
18
+ describe('downloadArticle', () => {
19
+ it('returns the saved markdown file path on success', async () => {
20
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-article-'));
21
+ tempDirs.push(tempDir);
22
+ const result = await downloadArticle({
23
+ title: 'Test Article',
24
+ author: 'Author',
25
+ publishTime: '2026-04-20 12:00:00',
26
+ sourceUrl: 'https://example.com/article',
27
+ contentHtml: '<p>Hello world</p>',
28
+ }, {
29
+ output: tempDir,
30
+ downloadImages: false,
31
+ });
32
+ expect(result).toHaveLength(1);
33
+ expect(result[0].status).toBe('success');
34
+ expect(result[0].saved).toMatch(new RegExp(`^${tempDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
35
+ expect(path.extname(result[0].saved)).toBe('.md');
36
+ expect(fs.existsSync(result[0].saved)).toBe(true);
37
+ expect(fs.readFileSync(result[0].saved, 'utf8')).toContain('Hello world');
38
+ });
39
+ });
@@ -185,6 +185,9 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
185
185
  throw new CommandExecutionError(`Pre-navigation to ${preNavUrl} failed: ${err instanceof Error ? err.message : err}`, 'Check that the site is reachable and the browser extension is running.');
186
186
  }
187
187
  }
188
+ // --live / OPENCLI_LIVE=1 keeps the automation window open after the
189
+ // command finishes, so agents (or humans) can inspect the page state.
190
+ const keepOpen = process.env.OPENCLI_LIVE === '1' || process.env.OPENCLI_LIVE === 'true';
188
191
  try {
189
192
  const result = await runWithTimeout(runCommand(cmd, page, kwargs, debug), {
190
193
  timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT,
@@ -192,7 +195,8 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
192
195
  });
193
196
  // Adapter commands are one-shot — close the automation window immediately
194
197
  // instead of waiting for the 30s idle timeout.
195
- await page.closeWindow?.().catch(() => { });
198
+ if (!keepOpen)
199
+ await page.closeWindow?.().catch(() => { });
196
200
  return result;
197
201
  }
198
202
  catch (err) {
@@ -206,7 +210,8 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
206
210
  // Close the automation window on failure too — without this, the window
207
211
  // lingers until the extension's idle timer fires (unreliable on Windows
208
212
  // where MV3 service workers may be suspended before setTimeout triggers).
209
- await page.closeWindow?.().catch(() => { });
213
+ if (!keepOpen)
214
+ await page.closeWindow?.().catch(() => { });
210
215
  throw err;
211
216
  }
212
217
  }, { workspace: `site:${cmd.site}`, cdpEndpoint });
@@ -60,6 +60,60 @@ describe('executeCommand — non-browser timeout', () => {
60
60
  expect(closeWindow).toHaveBeenCalledTimes(1);
61
61
  vi.restoreAllMocks();
62
62
  });
63
+ it('skips closeWindow when OPENCLI_LIVE=1 (success path)', async () => {
64
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
65
+ const mockPage = { closeWindow };
66
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
67
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
68
+ const prev = process.env.OPENCLI_LIVE;
69
+ process.env.OPENCLI_LIVE = '1';
70
+ try {
71
+ const cmd = cli({
72
+ site: 'test-execution',
73
+ name: 'browser-live-success',
74
+ description: 'test closeWindow skipped with --live on success',
75
+ browser: true,
76
+ strategy: Strategy.PUBLIC,
77
+ func: async () => [{ ok: true }],
78
+ });
79
+ await executeCommand(cmd, {});
80
+ expect(closeWindow).not.toHaveBeenCalled();
81
+ }
82
+ finally {
83
+ if (prev === undefined)
84
+ delete process.env.OPENCLI_LIVE;
85
+ else
86
+ process.env.OPENCLI_LIVE = prev;
87
+ vi.restoreAllMocks();
88
+ }
89
+ });
90
+ it('skips closeWindow when OPENCLI_LIVE=1 (failure path)', async () => {
91
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
92
+ const mockPage = { closeWindow };
93
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
94
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
95
+ const prev = process.env.OPENCLI_LIVE;
96
+ process.env.OPENCLI_LIVE = '1';
97
+ try {
98
+ const cmd = cli({
99
+ site: 'test-execution',
100
+ name: 'browser-live-failure',
101
+ description: 'test closeWindow skipped with --live on failure',
102
+ browser: true,
103
+ strategy: Strategy.PUBLIC,
104
+ func: async () => { throw new Error('adapter failure'); },
105
+ });
106
+ await expect(executeCommand(cmd, {})).rejects.toThrow('adapter failure');
107
+ expect(closeWindow).not.toHaveBeenCalled();
108
+ }
109
+ finally {
110
+ if (prev === undefined)
111
+ delete process.env.OPENCLI_LIVE;
112
+ else
113
+ process.env.OPENCLI_LIVE = prev;
114
+ vi.restoreAllMocks();
115
+ }
116
+ });
63
117
  it('does not re-run custom validation when args are already prepared', async () => {
64
118
  const validateArgs = vi.fn();
65
119
  const cmd = {
package/dist/src/main.js CHANGED
@@ -26,6 +26,22 @@ const __dirname = path.dirname(__filename);
26
26
  // Use findPackageRoot so the path works both in dev (src/main.ts) and prod (dist/src/main.js).
27
27
  const BUILTIN_CLIS = path.join(findPackageRoot(__filename), 'clis');
28
28
  const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
29
+ // ── Session lifecycle flags ──────────────────────────────────────────────
30
+ // `--live` / `--focus` are top-level-ish toggles that tweak the automation
31
+ // window's lifecycle. We strip them from argv before Commander runs so they
32
+ // can be placed anywhere and work on any subcommand (adapter or browser).
33
+ {
34
+ const liveIdx = process.argv.indexOf('--live');
35
+ if (liveIdx !== -1) {
36
+ process.env.OPENCLI_LIVE = '1';
37
+ process.argv.splice(liveIdx, 1);
38
+ }
39
+ const focusIdx = process.argv.indexOf('--focus');
40
+ if (focusIdx !== -1) {
41
+ process.env.OPENCLI_WINDOW_FOCUSED = '1';
42
+ process.argv.splice(focusIdx, 1);
43
+ }
44
+ }
29
45
  // ── Ultra-fast path: lightweight commands bypass full discovery ──────────
30
46
  // These are high-frequency or trivial paths that must not pay the startup tax.
31
47
  const argv = process.argv.slice(2);
@@ -60,13 +60,6 @@ declare function resolveStoredPluginSource(lockEntry: LockEntry | undefined, plu
60
60
  */
61
61
  type MoveDirFsOps = Pick<typeof fs, 'renameSync' | 'cpSync' | 'rmSync'>;
62
62
  declare function moveDir(src: string, dest: string, fsOps?: MoveDirFsOps): void;
63
- type PromoteDirFsOps = MoveDirFsOps & Pick<typeof fs, 'existsSync' | 'mkdirSync'>;
64
- /**
65
- * Promote a prepared staging directory into its final location.
66
- * The final path is only exposed after the directory has been fully prepared.
67
- */
68
- declare function promoteDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
69
- declare function replaceDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
70
63
  export interface ValidationResult {
71
64
  valid: boolean;
72
65
  errors: string[];
@@ -149,4 +142,4 @@ declare function parseSource(source: string): ParsedSource | null;
149
142
  */
150
143
  export declare function resolveEsbuildBin(): string | null;
151
144
  declare function resolveHostOpencliRoot(startFile?: string): string;
152
- export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, promoteDir as _promoteDir, replaceDir as _replaceDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
145
+ export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
@@ -159,32 +159,6 @@ function createSiblingTempPath(dest, kind) {
159
159
  const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
160
160
  return path.join(path.dirname(dest), `.${path.basename(dest)}.${kind}-${suffix}`);
161
161
  }
162
- /**
163
- * Promote a prepared staging directory into its final location.
164
- * The final path is only exposed after the directory has been fully prepared.
165
- */
166
- function promoteDir(stagingDir, dest, fsOps = fs) {
167
- if (fsOps.existsSync(dest)) {
168
- throw new PluginError(`Destination already exists: ${dest}`);
169
- }
170
- fsOps.mkdirSync(path.dirname(dest), { recursive: true });
171
- const tempDest = createSiblingTempPath(dest, 'tmp');
172
- try {
173
- moveDir(stagingDir, tempDest, fsOps);
174
- fsOps.renameSync(tempDest, dest);
175
- }
176
- catch (err) {
177
- try {
178
- fsOps.rmSync(tempDest, { recursive: true, force: true });
179
- }
180
- catch { }
181
- throw err;
182
- }
183
- }
184
- function replaceDir(stagingDir, dest, fsOps = fs) {
185
- const replacement = beginReplaceDir(stagingDir, dest, fsOps);
186
- replacement.finalize();
187
- }
188
162
  function cloneRepoToTemp(cloneUrl) {
189
163
  const tmpCloneDir = path.join(os.tmpdir(), `opencli-clone-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
190
164
  try {
@@ -1268,4 +1242,4 @@ function transpilePluginTs(pluginDir) {
1268
1242
  log.warn(`TS transpilation setup failed: ${getErrorMessage(err)}`);
1269
1243
  }
1270
1244
  }
1271
- export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, promoteDir as _promoteDir, replaceDir as _replaceDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
1245
+ export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
@@ -12,7 +12,7 @@ const { mockExecFileSync, mockExecSync } = vi.hoisted(() => ({
12
12
  mockExecFileSync: vi.fn(),
13
13
  mockExecSync: vi.fn(),
14
14
  }));
15
- const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, _promoteDir, _replaceDir, installPlugin, listPlugins, _readLockFile, _readLockFileWithWriter, _resolveEsbuildBin, _resolveHostOpencliRoot, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
15
+ const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, installPlugin, listPlugins, _readLockFile, _readLockFileWithWriter, _resolveEsbuildBin, _resolveHostOpencliRoot, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
16
16
  describe('parseSource', () => {
17
17
  it('parses github:user/repo format', () => {
18
18
  const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
@@ -924,64 +924,6 @@ describe('moveDir', () => {
924
924
  expect(rmSync).toHaveBeenCalledWith(dest, { recursive: true, force: true });
925
925
  });
926
926
  });
927
- describe('promoteDir', () => {
928
- it('cleans up temporary publish dir when final rename fails', () => {
929
- const staging = path.join(os.tmpdir(), 'opencli-promote-stage');
930
- const dest = path.join(os.tmpdir(), 'opencli-promote-dest');
931
- const publishErr = new Error('publish failed');
932
- const existsSync = vi.fn(() => false);
933
- const mkdirSync = vi.fn(() => undefined);
934
- const cpSync = vi.fn(() => undefined);
935
- const rmSync = vi.fn(() => undefined);
936
- const renameSync = vi.fn((src, _target) => {
937
- if (String(src) === staging)
938
- return;
939
- throw publishErr;
940
- });
941
- expect(() => _promoteDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
942
- const tempDest = renameSync.mock.calls[0][1];
943
- expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
944
- expect(renameSync).toHaveBeenNthCalledWith(2, tempDest, dest);
945
- expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
946
- });
947
- });
948
- describe('replaceDir', () => {
949
- it('rolls back the original destination when swap fails', () => {
950
- const staging = path.join(os.tmpdir(), 'opencli-replace-stage');
951
- const dest = path.join(os.tmpdir(), 'opencli-replace-dest');
952
- const publishErr = new Error('swap failed');
953
- const existingPaths = new Set([dest]);
954
- const existsSync = vi.fn((p) => existingPaths.has(String(p)));
955
- const mkdirSync = vi.fn(() => undefined);
956
- const cpSync = vi.fn(() => undefined);
957
- const rmSync = vi.fn(() => undefined);
958
- const renameSync = vi.fn((src, target) => {
959
- if (String(src) === staging) {
960
- existingPaths.add(String(target));
961
- return;
962
- }
963
- if (String(src) === dest) {
964
- existingPaths.delete(dest);
965
- existingPaths.add(String(target));
966
- return;
967
- }
968
- if (String(target) === dest)
969
- throw publishErr;
970
- if (existingPaths.has(String(src))) {
971
- existingPaths.delete(String(src));
972
- existingPaths.add(String(target));
973
- }
974
- });
975
- expect(() => _replaceDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
976
- const tempDest = renameSync.mock.calls[0][1];
977
- const backupDest = renameSync.mock.calls[1][1];
978
- expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
979
- expect(renameSync).toHaveBeenNthCalledWith(2, dest, backupDest);
980
- expect(renameSync).toHaveBeenNthCalledWith(3, tempDest, dest);
981
- expect(renameSync).toHaveBeenNthCalledWith(4, backupDest, dest);
982
- expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
983
- });
984
- });
985
927
  describe('installPlugin transactional staging', () => {
986
928
  const standaloneSource = 'github:user/opencli-plugin-__test-transactional-standalone__';
987
929
  const standaloneName = '__test-transactional-standalone__';
@@ -4,6 +4,7 @@
4
4
  import type { IPage } from './types.js';
5
5
  export declare enum Strategy {
6
6
  PUBLIC = "public",
7
+ LOCAL = "local",
7
8
  COOKIE = "cookie",
8
9
  HEADER = "header",
9
10
  INTERCEPT = "intercept",
@@ -4,6 +4,7 @@
4
4
  export var Strategy;
5
5
  (function (Strategy) {
6
6
  Strategy["PUBLIC"] = "public";
7
+ Strategy["LOCAL"] = "local";
7
8
  Strategy["COOKIE"] = "cookie";
8
9
  Strategy["HEADER"] = "header";
9
10
  Strategy["INTERCEPT"] = "intercept";
@@ -58,13 +59,13 @@ export function strategyLabel(cmd) {
58
59
  */
59
60
  function normalizeCommand(cmd) {
60
61
  const strategy = cmd.strategy ?? (cmd.browser === false ? Strategy.PUBLIC : Strategy.COOKIE);
61
- const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC);
62
+ const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL);
62
63
  let navigateBefore = cmd.navigateBefore;
63
64
  if (navigateBefore === undefined) {
64
65
  if ((strategy === Strategy.COOKIE || strategy === Strategy.HEADER) && cmd.domain) {
65
66
  navigateBefore = `https://${cmd.domain}`;
66
67
  }
67
- else if (strategy !== Strategy.PUBLIC) {
68
+ else if (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL) {
68
69
  // Non-PUBLIC without domain: needs authenticated browser context
69
70
  // but no specific pre-navigation URL. `true` signals this to
70
71
  // shouldUseBrowserSession without triggering resolvePreNav.
@@ -43,6 +43,17 @@ describe('cli() registration', () => {
43
43
  });
44
44
  expect(cmd.strategy).toBe(Strategy.PUBLIC);
45
45
  });
46
+ it('preserves LOCAL strategy on registration', () => {
47
+ const cmd = cli({
48
+ site: 'test-registry',
49
+ name: 'local-strategy',
50
+ description: 'reads local credentials',
51
+ strategy: Strategy.LOCAL,
52
+ browser: false,
53
+ });
54
+ expect(cmd.strategy).toBe(Strategy.LOCAL);
55
+ expect(cmd.browser).toBe(false);
56
+ });
46
57
  it('overwrites existing command on re-registration', () => {
47
58
  cli({ site: 'test-registry', name: 'overwrite', description: 'v1' });
48
59
  cli({ site: 'test-registry', name: 'overwrite', description: 'v2' });
@@ -148,6 +159,17 @@ describe('normalizeCommand (via registerCommand)', () => {
148
159
  expect(cmd.browser).toBe(false);
149
160
  expect(cmd.navigateBefore).toBeUndefined();
150
161
  });
162
+ it('LOCAL → browser false, navigateBefore undefined', () => {
163
+ registerCommand({
164
+ site: 'test-norm', name: 'local', description: '', args: [],
165
+ strategy: Strategy.LOCAL,
166
+ });
167
+ const cmd = getRegistry().get('test-norm/local');
168
+ expect(cmd.strategy).toBe(Strategy.LOCAL);
169
+ expect(strategyLabel(cmd)).toBe('local');
170
+ expect(cmd.browser).toBe(false);
171
+ expect(cmd.navigateBefore).toBeUndefined();
172
+ });
151
173
  it('explicit navigateBefore: false overrides COOKIE + domain', () => {
152
174
  registerCommand({
153
175
  site: 'test-norm', name: 'cookie-override', description: '', args: [],
@@ -51,16 +51,31 @@ export interface IPage {
51
51
  url?: string;
52
52
  }): Promise<BrowserCookie[]>;
53
53
  snapshot(opts?: SnapshotOptions): Promise<any>;
54
- click(ref: string): Promise<void>;
55
- typeText(ref: string, text: string): Promise<void>;
54
+ click(ref: string, opts?: {
55
+ nth?: number;
56
+ firstOnMulti?: boolean;
57
+ }): Promise<{
58
+ matches_n: number;
59
+ match_level: 'exact' | 'stable' | 'reidentified';
60
+ }>;
61
+ typeText(ref: string, text: string, opts?: {
62
+ nth?: number;
63
+ firstOnMulti?: boolean;
64
+ }): Promise<{
65
+ matches_n: number;
66
+ match_level: 'exact' | 'stable' | 'reidentified';
67
+ }>;
56
68
  pressKey(key: string): Promise<void>;
57
- scrollTo(ref: string): Promise<any>;
69
+ scrollTo(ref: string, opts?: {
70
+ nth?: number;
71
+ firstOnMulti?: boolean;
72
+ }): Promise<any>;
58
73
  getFormState(): Promise<any>;
59
74
  wait(options: number | WaitOptions): Promise<void>;
60
75
  tabs(): Promise<any>;
61
- closeTab?(index?: number): Promise<void>;
62
- newTab?(): Promise<void>;
63
- selectTab(index: number): Promise<void>;
76
+ closeTab?(target?: number | string): Promise<void>;
77
+ newTab?(url?: string): Promise<string | undefined>;
78
+ selectTab(target: number | string): Promise<void>;
64
79
  networkRequests(includeStatic?: boolean): Promise<any>;
65
80
  consoleMessages(level?: string): Promise<any>;
66
81
  scroll(direction?: string, amount?: number): Promise<void>;
@@ -89,10 +104,19 @@ export interface IPage {
89
104
  getCurrentUrl?(): Promise<string | null>;
90
105
  /** Returns the active page identity (targetId), or undefined if not yet resolved. */
91
106
  getActivePage?(): string | undefined;
92
- /** @deprecated Use getActivePage() instead */
93
- getActiveTabId?(): number | undefined;
107
+ /** Bind the page object to a specific page identity (targetId). */
108
+ setActivePage?(page?: string): void;
94
109
  /** Send a raw CDP command via chrome.debugger passthrough. */
95
110
  cdp?(method: string, params?: Record<string, unknown>): Promise<unknown>;
111
+ /** List cross-origin iframe targets in snapshot order. */
112
+ frames?(): Promise<Array<{
113
+ index: number;
114
+ frameId: string;
115
+ url: string;
116
+ name: string;
117
+ }>>;
118
+ /** Evaluate JavaScript inside a cross-origin iframe identified by its frame index. */
119
+ evaluateInFrame?(js: string, frameIndex: number): Promise<unknown>;
96
120
  /** Click at native coordinates via CDP Input.dispatchMouseEvent. */
97
121
  nativeClick?(x: number, y: number): Promise<void>;
98
122
  /** Type text via CDP Input.insertText. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,77 +0,0 @@
1
- const MEMBER_PATTERNS = [
2
- /([\d.,]+(?:\s?[KMB千萬万亿])?)\s*members?/i,
3
- /([\d.,]+(?:\s?[KMB千萬万亿])?)\s*位成员/,
4
- ];
5
- const FOLLOWER_PATTERNS = [
6
- /([\d.,]+(?:\s?[KMB千萬万亿])?)\s*followers?/i,
7
- /([\d.,]+(?:\s?[KMB千萬万亿])?)\s*位关注者/,
8
- ];
9
- const PRIVATE_PATTERNS = [/\bprivate\b/i, /锁定列表/];
10
- const EMPTY_STATE_PATTERNS = [
11
- /hasn't created any lists/i,
12
- /has not created any lists/i,
13
- /no lists yet/i,
14
- /没有创建任何列表/,
15
- /还没有创建任何列表/,
16
- ];
17
- function normalizeText(text) {
18
- return String(text || '').replace(/\s+/g, ' ').trim();
19
- }
20
- function matchMetric(text, patterns) {
21
- for (const pattern of patterns) {
22
- const match = text.match(pattern);
23
- if (match)
24
- return normalizeText(match[1]);
25
- }
26
- return '0';
27
- }
28
- function looksLikeMetadata(line) {
29
- const text = normalizeText(line);
30
- if (!text)
31
- return true;
32
- if (text.startsWith('@'))
33
- return true;
34
- if (MEMBER_PATTERNS.some((pattern) => pattern.test(text)))
35
- return true;
36
- if (FOLLOWER_PATTERNS.some((pattern) => pattern.test(text)))
37
- return true;
38
- if (PRIVATE_PATTERNS.some((pattern) => pattern.test(text)))
39
- return true;
40
- if (/^(public|pinned)$/i.test(text))
41
- return true;
42
- if (/^(lists?|你的列表)$/i.test(text))
43
- return true;
44
- return false;
45
- }
46
- export function parseListCards(cards) {
47
- const seen = new Set();
48
- const results = [];
49
- for (const card of cards || []) {
50
- const href = normalizeText(card?.href);
51
- const rawText = String(card?.text || '');
52
- if (!href || seen.has(href))
53
- continue;
54
- seen.add(href);
55
- const text = normalizeText(rawText);
56
- if (!text)
57
- continue;
58
- const lines = rawText
59
- .split('\n')
60
- .map((line) => normalizeText(line))
61
- .filter(Boolean);
62
- const name = lines.find((line) => !looksLikeMetadata(line));
63
- if (!name)
64
- continue;
65
- results.push({
66
- name,
67
- members: matchMetric(text, MEMBER_PATTERNS),
68
- followers: matchMetric(text, FOLLOWER_PATTERNS),
69
- mode: PRIVATE_PATTERNS.some((pattern) => pattern.test(text)) ? 'private' : 'public',
70
- });
71
- }
72
- return results;
73
- }
74
- export function isEmptyListsState(text) {
75
- const normalized = normalizeText(text);
76
- return EMPTY_STATE_PATTERNS.some((pattern) => pattern.test(normalized));
77
- }
@@ -1,5 +0,0 @@
1
- import { Argument, Column } from '@jackwener/opencli/types';
2
- declare const args: Argument[];
3
- declare const columns: Column[];
4
- export { args, columns };
5
- export default {};