@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.
- package/README.md +76 -51
- package/README.zh-CN.md +78 -62
- package/cli-manifest.json +4558 -2979
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/video.js +61 -0
- package/clis/bilibili/video.test.js +81 -0
- package/clis/deepseek/ask.js +94 -0
- package/clis/deepseek/ask.test.js +73 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +291 -0
- package/clis/deepseek/utils.test.js +37 -0
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/jianyu/search.js +139 -3
- package/clis/jianyu/search.test.js +25 -0
- package/clis/jianyu/shared/procurement-detail.js +15 -0
- package/clis/jianyu/shared/procurement-detail.test.js +12 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/twitter/shared.js +7 -2
- package/clis/twitter/tweets.js +218 -0
- package/clis/twitter/tweets.test.js +125 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/download.js +8 -4
- package/clis/xiaoyuzhou/download.test.js +23 -13
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/channel.js +35 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +14 -4
- package/dist/src/browser/base-page.js +35 -25
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +1 -1
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +13 -4
- package/dist/src/browser/compound.d.ts +59 -0
- package/dist/src/browser/compound.js +112 -0
- package/dist/src/browser/compound.test.js +175 -0
- package/dist/src/browser/daemon-client.d.ts +6 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.d.ts +7 -0
- package/dist/src/browser/dom-snapshot.js +83 -5
- package/dist/src/browser/dom-snapshot.test.js +65 -0
- package/dist/src/browser/extract.d.ts +69 -0
- package/dist/src/browser/extract.js +132 -0
- package/dist/src/browser/extract.test.js +129 -0
- package/dist/src/browser/find.d.ts +76 -0
- package/dist/src/browser/find.js +179 -0
- package/dist/src/browser/find.test.js +120 -0
- package/dist/src/browser/html-tree.d.ts +75 -0
- package/dist/src/browser/html-tree.js +112 -0
- package/dist/src/browser/html-tree.test.d.ts +1 -0
- package/dist/src/browser/html-tree.test.js +181 -0
- package/dist/src/browser/network-cache.d.ts +48 -0
- package/dist/src/browser/network-cache.js +66 -0
- package/dist/src/browser/network-cache.test.d.ts +1 -0
- package/dist/src/browser/network-cache.test.js +58 -0
- package/dist/src/browser/network-key.d.ts +22 -0
- package/dist/src/browser/network-key.js +66 -0
- package/dist/src/browser/network-key.test.d.ts +1 -0
- package/dist/src/browser/network-key.test.js +49 -0
- package/dist/src/browser/page.d.ts +14 -4
- package/dist/src/browser/page.js +48 -7
- package/dist/src/browser/page.test.js +97 -0
- package/dist/src/browser/shape-filter.d.ts +52 -0
- package/dist/src/browser/shape-filter.js +101 -0
- package/dist/src/browser/shape-filter.test.d.ts +1 -0
- package/dist/src/browser/shape-filter.test.js +101 -0
- package/dist/src/browser/shape.d.ts +23 -0
- package/dist/src/browser/shape.js +95 -0
- package/dist/src/browser/shape.test.d.ts +1 -0
- package/dist/src/browser/shape.test.js +82 -0
- package/dist/src/browser/target-errors.d.ts +14 -1
- package/dist/src/browser/target-errors.js +13 -0
- package/dist/src/browser/target-errors.test.js +39 -6
- package/dist/src/browser/target-resolver.d.ts +57 -10
- package/dist/src/browser/target-resolver.js +195 -75
- package/dist/src/browser/target-resolver.test.js +80 -5
- package/dist/src/cli.js +849 -267
- package/dist/src/cli.test.js +961 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +8 -0
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.d.ts +1 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/execution.js +7 -2
- package/dist/src/execution.test.js +54 -0
- package/dist/src/main.js +16 -0
- package/dist/src/plugin.d.ts +1 -8
- package/dist/src/plugin.js +1 -27
- package/dist/src/plugin.test.js +1 -59
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +32 -8
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/compound.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/extract.test.d.ts} +0 -0
- /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,
|
|
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] =
|
|
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
|
-
//
|
|
64
|
+
// prepareCommandArgs validates bools before dispatch; executeCommand should not be reached
|
|
65
65
|
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
66
66
|
});
|
|
67
67
|
});
|
package/dist/src/daemon.js
CHANGED
|
@@ -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);
|
|
@@ -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
|
+
});
|
package/dist/src/execution.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -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,
|
|
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, };
|
package/dist/src/plugin.js
CHANGED
|
@@ -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,
|
|
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, };
|
package/dist/src/plugin.test.js
CHANGED
|
@@ -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,
|
|
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__';
|
package/dist/src/registry.d.ts
CHANGED
package/dist/src/registry.js
CHANGED
|
@@ -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: [],
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
|
55
|
-
|
|
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
|
|
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?(
|
|
62
|
-
newTab?(): Promise<
|
|
63
|
-
selectTab(
|
|
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
|
-
/**
|
|
93
|
-
|
|
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,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
|
-
}
|