@jackwener/opencli 1.5.2 → 1.5.4
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/.agents/skills/cross-project-adapter-migration/SKILL.md +3 -3
- package/.github/workflows/ci.yml +6 -7
- package/README.md +89 -235
- package/dist/browser/cdp.js +20 -1
- package/dist/browser/daemon-client.js +3 -2
- package/dist/browser/dom-helpers.d.ts +11 -0
- package/dist/browser/dom-helpers.js +42 -0
- package/dist/browser/dom-helpers.test.d.ts +1 -0
- package/dist/browser/dom-helpers.test.js +92 -0
- package/dist/browser/index.d.ts +0 -12
- package/dist/browser/index.js +0 -13
- package/dist/browser/mcp.js +4 -3
- package/dist/browser/page.d.ts +1 -0
- package/dist/browser/page.js +14 -1
- package/dist/browser.test.js +15 -11
- package/dist/build-manifest.d.ts +2 -3
- package/dist/build-manifest.js +75 -170
- package/dist/build-manifest.test.js +113 -88
- package/dist/cli-manifest.json +1199 -1106
- package/dist/clis/36kr/hot.js +1 -1
- package/dist/clis/36kr/search.js +1 -1
- package/dist/clis/_shared/common.d.ts +8 -0
- package/dist/clis/_shared/common.js +10 -0
- package/dist/clis/bloomberg/news.js +1 -1
- package/dist/clis/douban/utils.js +3 -6
- package/dist/clis/medium/utils.js +1 -1
- package/dist/clis/producthunt/browse.js +1 -1
- package/dist/clis/producthunt/hot.js +1 -1
- package/dist/clis/sinablog/utils.js +6 -7
- package/dist/clis/substack/utils.js +2 -2
- package/dist/clis/twitter/block.js +1 -1
- package/dist/clis/twitter/bookmark.js +1 -1
- package/dist/clis/twitter/delete.js +1 -1
- package/dist/clis/twitter/follow.js +1 -1
- package/dist/clis/twitter/followers.js +2 -2
- package/dist/clis/twitter/following.js +2 -2
- package/dist/clis/twitter/hide-reply.js +1 -1
- package/dist/clis/twitter/like.js +1 -1
- package/dist/clis/twitter/notifications.js +1 -1
- package/dist/clis/twitter/profile.js +1 -1
- package/dist/clis/twitter/reply-dm.js +1 -1
- package/dist/clis/twitter/reply.js +1 -1
- package/dist/clis/twitter/search.js +1 -1
- package/dist/clis/twitter/unblock.js +1 -1
- package/dist/clis/twitter/unbookmark.js +1 -1
- package/dist/clis/twitter/unfollow.js +1 -1
- package/dist/clis/xiaohongshu/comments.test.js +1 -0
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +1 -0
- package/dist/clis/xiaohongshu/creator-notes.test.js +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +1 -0
- package/dist/clis/xiaohongshu/search.test.js +1 -0
- package/dist/daemon.js +14 -3
- package/dist/download/index.js +39 -33
- package/dist/download/index.test.js +15 -1
- package/dist/execution.js +3 -2
- package/dist/external-clis.yaml +16 -0
- package/dist/main.js +2 -0
- package/dist/node-network.d.ts +10 -0
- package/dist/node-network.js +174 -0
- package/dist/node-network.test.d.ts +1 -0
- package/dist/node-network.test.js +55 -0
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.test.js +1 -0
- package/dist/pipeline/steps/intercept.js +4 -5
- package/dist/serialization.js +6 -1
- package/dist/serialization.test.d.ts +1 -0
- package/dist/serialization.test.js +23 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/docs/superpowers/plans/2026-03-28-perf-smart-wait.md +1143 -0
- package/docs/superpowers/specs/2026-03-28-perf-smart-wait-design.md +170 -0
- package/extension/dist/background.js +12 -5
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +2 -2
- package/extension/package.json +1 -1
- package/extension/src/background.ts +20 -6
- package/extension/src/protocol.ts +2 -1
- package/package.json +2 -1
- package/src/browser/cdp.ts +21 -0
- package/src/browser/daemon-client.ts +3 -2
- package/src/browser/dom-helpers.test.ts +100 -0
- package/src/browser/dom-helpers.ts +44 -0
- package/src/browser/index.ts +0 -15
- package/src/browser/mcp.ts +4 -3
- package/src/browser/page.ts +16 -0
- package/src/browser.test.ts +16 -12
- package/src/build-manifest.test.ts +117 -88
- package/src/build-manifest.ts +81 -180
- package/src/clis/36kr/hot.ts +1 -1
- package/src/clis/36kr/search.ts +1 -1
- package/src/clis/_shared/common.ts +11 -0
- package/src/clis/bloomberg/news.ts +1 -1
- package/src/clis/douban/utils.ts +3 -7
- package/src/clis/medium/utils.ts +1 -1
- package/src/clis/producthunt/browse.ts +1 -1
- package/src/clis/producthunt/hot.ts +1 -1
- package/src/clis/sinablog/utils.ts +6 -7
- package/src/clis/substack/utils.ts +2 -2
- package/src/clis/twitter/block.ts +1 -1
- package/src/clis/twitter/bookmark.ts +1 -1
- package/src/clis/twitter/delete.ts +1 -1
- package/src/clis/twitter/follow.ts +1 -1
- package/src/clis/twitter/followers.ts +2 -2
- package/src/clis/twitter/following.ts +2 -2
- package/src/clis/twitter/hide-reply.ts +1 -1
- package/src/clis/twitter/like.ts +1 -1
- package/src/clis/twitter/notifications.ts +1 -1
- package/src/clis/twitter/profile.ts +1 -1
- package/src/clis/twitter/reply-dm.ts +1 -1
- package/src/clis/twitter/reply.ts +1 -1
- package/src/clis/twitter/search.ts +1 -1
- package/src/clis/twitter/unblock.ts +1 -1
- package/src/clis/twitter/unbookmark.ts +1 -1
- package/src/clis/twitter/unfollow.ts +1 -1
- package/src/clis/xiaohongshu/comments.test.ts +1 -0
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +1 -0
- package/src/clis/xiaohongshu/creator-notes.test.ts +1 -0
- package/src/clis/xiaohongshu/publish.test.ts +1 -0
- package/src/clis/xiaohongshu/search.test.ts +1 -0
- package/src/daemon.ts +16 -4
- package/src/download/index.test.ts +19 -1
- package/src/download/index.ts +50 -41
- package/src/execution.ts +3 -2
- package/src/external-clis.yaml +16 -0
- package/src/main.ts +3 -0
- package/src/node-network.test.ts +93 -0
- package/src/node-network.ts +213 -0
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.test.ts +1 -0
- package/src/pipeline/steps/intercept.ts +4 -5
- package/src/serialization.test.ts +26 -0
- package/src/serialization.ts +6 -1
- package/src/types.ts +2 -0
- package/src/utils.ts +5 -0
package/src/browser.test.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BrowserBridge,
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { BrowserBridge, generateStealthJs } from './browser/index.js';
|
|
3
|
+
import { extractTabEntries, diffTabIndexes, appendLimited } from './browser/tabs.js';
|
|
4
|
+
import { withTimeoutMs } from './runtime.js';
|
|
5
|
+
import { __test__ as cdpTest } from './browser/cdp.js';
|
|
6
|
+
import { isRetryableSettleError } from './browser/page.js';
|
|
3
7
|
import * as daemonClient from './browser/daemon-client.js';
|
|
4
8
|
|
|
5
9
|
describe('browser helpers', () => {
|
|
6
10
|
it('extracts tab entries from string snapshots', () => {
|
|
7
|
-
const entries =
|
|
11
|
+
const entries = extractTabEntries('Tab 0 https://example.com\nTab 1 Chrome Extension');
|
|
8
12
|
|
|
9
13
|
expect(entries).toEqual([
|
|
10
14
|
{ index: 0, identity: 'https://example.com' },
|
|
@@ -13,7 +17,7 @@ describe('browser helpers', () => {
|
|
|
13
17
|
});
|
|
14
18
|
|
|
15
19
|
it('extracts tab entries from MCP markdown format', () => {
|
|
16
|
-
const entries =
|
|
20
|
+
const entries = extractTabEntries(
|
|
17
21
|
'- 0: (current) [Playwright MCP extension](chrome-extension://abc/connect.html)\n- 1: [知乎 - 首页](https://www.zhihu.com/)'
|
|
18
22
|
);
|
|
19
23
|
|
|
@@ -24,7 +28,7 @@ describe('browser helpers', () => {
|
|
|
24
28
|
});
|
|
25
29
|
|
|
26
30
|
it('closes only tabs that were opened during the session', () => {
|
|
27
|
-
const tabsToClose =
|
|
31
|
+
const tabsToClose = diffTabIndexes(
|
|
28
32
|
['https://example.com', 'Chrome Extension'],
|
|
29
33
|
[
|
|
30
34
|
{ index: 0, identity: 'https://example.com' },
|
|
@@ -38,21 +42,21 @@ describe('browser helpers', () => {
|
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
it('keeps only the tail of stderr buffers', () => {
|
|
41
|
-
expect(
|
|
45
|
+
expect(appendLimited('12345', '67890', 8)).toBe('34567890');
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
it('times out slow promises', async () => {
|
|
45
|
-
await expect(
|
|
49
|
+
await expect(withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout');
|
|
46
50
|
});
|
|
47
51
|
|
|
48
52
|
it('retries settle only for target-invalidated errors', () => {
|
|
49
|
-
expect(
|
|
50
|
-
expect(
|
|
51
|
-
expect(
|
|
53
|
+
expect(isRetryableSettleError(new Error('{"code":-32000,"message":"Inspected target navigated or closed"}'))).toBe(true);
|
|
54
|
+
expect(isRetryableSettleError(new Error('attach failed: target no longer exists'))).toBe(false);
|
|
55
|
+
expect(isRetryableSettleError(new Error('malformed exec payload'))).toBe(false);
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
it('prefers the real Electron app target over DevTools and blank pages', () => {
|
|
55
|
-
const target =
|
|
59
|
+
const target = cdpTest.selectCDPTarget([
|
|
56
60
|
{
|
|
57
61
|
type: 'page',
|
|
58
62
|
title: 'DevTools - localhost:9224',
|
|
@@ -79,7 +83,7 @@ describe('browser helpers', () => {
|
|
|
79
83
|
it('honors OPENCLI_CDP_TARGET when multiple inspectable targets exist', () => {
|
|
80
84
|
vi.stubEnv('OPENCLI_CDP_TARGET', 'codex');
|
|
81
85
|
|
|
82
|
-
const target =
|
|
86
|
+
const target = cdpTest.selectCDPTarget([
|
|
83
87
|
{
|
|
84
88
|
type: 'app',
|
|
85
89
|
title: 'Cursor',
|
|
@@ -2,69 +2,8 @@ import { afterEach, describe, expect, it } from 'vitest';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
describe('parseTsArgsBlock', () => {
|
|
8
|
-
it('keeps args with nested choices arrays', () => {
|
|
9
|
-
const args = parseTsArgsBlock(`
|
|
10
|
-
{
|
|
11
|
-
name: 'period',
|
|
12
|
-
type: 'string',
|
|
13
|
-
default: 'seven',
|
|
14
|
-
help: 'Stats period: seven or thirty',
|
|
15
|
-
choices: ['seven', 'thirty'],
|
|
16
|
-
},
|
|
17
|
-
`);
|
|
18
|
-
|
|
19
|
-
expect(args).toEqual([
|
|
20
|
-
{
|
|
21
|
-
name: 'period',
|
|
22
|
-
type: 'string',
|
|
23
|
-
default: 'seven',
|
|
24
|
-
required: false,
|
|
25
|
-
positional: undefined,
|
|
26
|
-
help: 'Stats period: seven or thirty',
|
|
27
|
-
choices: ['seven', 'thirty'],
|
|
28
|
-
},
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('keeps hyphenated arg names from TS adapters', () => {
|
|
33
|
-
const args = parseTsArgsBlock(`
|
|
34
|
-
{
|
|
35
|
-
name: 'tweet-url',
|
|
36
|
-
help: 'Single tweet URL to download',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'download-images',
|
|
40
|
-
type: 'boolean',
|
|
41
|
-
default: false,
|
|
42
|
-
help: 'Download images locally',
|
|
43
|
-
},
|
|
44
|
-
`);
|
|
45
|
-
|
|
46
|
-
expect(args).toEqual([
|
|
47
|
-
{
|
|
48
|
-
name: 'tweet-url',
|
|
49
|
-
type: 'str',
|
|
50
|
-
default: undefined,
|
|
51
|
-
required: false,
|
|
52
|
-
positional: undefined,
|
|
53
|
-
help: 'Single tweet URL to download',
|
|
54
|
-
choices: undefined,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'download-images',
|
|
58
|
-
type: 'boolean',
|
|
59
|
-
default: false,
|
|
60
|
-
required: false,
|
|
61
|
-
positional: undefined,
|
|
62
|
-
help: 'Download images locally',
|
|
63
|
-
choices: undefined,
|
|
64
|
-
},
|
|
65
|
-
]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
5
|
+
import { cli, getRegistry, Strategy } from './registry.js';
|
|
6
|
+
import { loadTsManifestEntries, shouldReplaceManifestEntry } from './build-manifest.js';
|
|
68
7
|
|
|
69
8
|
describe('manifest helper rules', () => {
|
|
70
9
|
const tempDirs: string[] = [];
|
|
@@ -127,43 +66,133 @@ describe('manifest helper rules', () => {
|
|
|
127
66
|
const file = path.join(dir, 'utils.ts');
|
|
128
67
|
fs.writeFileSync(file, `export function helper() { return 'noop'; }`);
|
|
129
68
|
|
|
130
|
-
expect(
|
|
69
|
+
return expect(loadTsManifestEntries(file, 'demo', async () => ({}))).resolves.toEqual([]);
|
|
131
70
|
});
|
|
132
71
|
|
|
133
|
-
it('
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
72
|
+
it('builds TS manifest entries from exported runtime commands', async () => {
|
|
73
|
+
const site = `manifest-hydrate-${Date.now()}`;
|
|
74
|
+
const key = `${site}/dynamic`;
|
|
75
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-'));
|
|
76
|
+
tempDirs.push(dir);
|
|
77
|
+
const file = path.join(dir, `${site}.ts`);
|
|
78
|
+
fs.writeFileSync(file, `export const command = cli({ site: '${site}', name: 'dynamic' });`);
|
|
79
|
+
|
|
80
|
+
const entries = await loadTsManifestEntries(file, site, async () => ({
|
|
81
|
+
command: cli({
|
|
82
|
+
site,
|
|
83
|
+
name: 'dynamic',
|
|
84
|
+
description: 'dynamic command',
|
|
85
|
+
strategy: Strategy.PUBLIC,
|
|
86
|
+
browser: false,
|
|
87
|
+
args: [
|
|
88
|
+
{
|
|
89
|
+
name: 'model',
|
|
90
|
+
required: true,
|
|
91
|
+
positional: true,
|
|
92
|
+
help: 'Choose a model',
|
|
93
|
+
choices: ['auto', 'thinking'],
|
|
94
|
+
default: '30',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
domain: 'localhost',
|
|
98
|
+
navigateBefore: 'https://example.com/session',
|
|
99
|
+
deprecated: 'legacy command',
|
|
100
|
+
replacedBy: 'opencli demo new',
|
|
101
|
+
}),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
expect(entries).toEqual([
|
|
105
|
+
{
|
|
106
|
+
site,
|
|
107
|
+
name: 'dynamic',
|
|
108
|
+
description: 'dynamic command',
|
|
109
|
+
domain: 'localhost',
|
|
110
|
+
strategy: 'public',
|
|
111
|
+
browser: false,
|
|
112
|
+
args: [
|
|
113
|
+
{
|
|
114
|
+
name: 'model',
|
|
115
|
+
type: 'str',
|
|
116
|
+
required: true,
|
|
117
|
+
positional: true,
|
|
118
|
+
help: 'Choose a model',
|
|
119
|
+
choices: ['auto', 'thinking'],
|
|
120
|
+
default: '30',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
type: 'ts',
|
|
124
|
+
modulePath: `${site}/${site}.js`,
|
|
125
|
+
navigateBefore: 'https://example.com/session',
|
|
126
|
+
deprecated: 'legacy command',
|
|
127
|
+
replacedBy: 'opencli demo new',
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
getRegistry().delete(key);
|
|
145
132
|
});
|
|
146
133
|
|
|
147
|
-
it('
|
|
134
|
+
it('falls back to registry delta for side-effect-only cli modules', async () => {
|
|
135
|
+
const site = `manifest-side-effect-${Date.now()}`;
|
|
136
|
+
const key = `${site}/legacy`;
|
|
148
137
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-'));
|
|
149
138
|
tempDirs.push(dir);
|
|
150
|
-
const file = path.join(dir,
|
|
151
|
-
fs.writeFileSync(file, `
|
|
152
|
-
|
|
139
|
+
const file = path.join(dir, `${site}.ts`);
|
|
140
|
+
fs.writeFileSync(file, `cli({ site: '${site}', name: 'legacy' });`);
|
|
141
|
+
|
|
142
|
+
const entries = await loadTsManifestEntries(file, site, async () => {
|
|
153
143
|
cli({
|
|
154
|
-
site
|
|
144
|
+
site,
|
|
155
145
|
name: 'legacy',
|
|
156
146
|
description: 'legacy command',
|
|
157
147
|
deprecated: 'legacy is deprecated',
|
|
158
148
|
replacedBy: 'opencli demo new',
|
|
159
149
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(scanTs(file, 'demo')).toMatchObject({
|
|
163
|
-
site: 'demo',
|
|
164
|
-
name: 'legacy',
|
|
165
|
-
deprecated: 'legacy is deprecated',
|
|
166
|
-
replacedBy: 'opencli demo new',
|
|
150
|
+
return {};
|
|
167
151
|
});
|
|
152
|
+
|
|
153
|
+
expect(entries).toEqual([
|
|
154
|
+
{
|
|
155
|
+
site,
|
|
156
|
+
name: 'legacy',
|
|
157
|
+
description: 'legacy command',
|
|
158
|
+
strategy: 'cookie',
|
|
159
|
+
browser: true,
|
|
160
|
+
args: [],
|
|
161
|
+
type: 'ts',
|
|
162
|
+
modulePath: `${site}/${site}.js`,
|
|
163
|
+
deprecated: 'legacy is deprecated',
|
|
164
|
+
replacedBy: 'opencli demo new',
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
getRegistry().delete(key);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('keeps every command a module exports instead of guessing by site', async () => {
|
|
172
|
+
const site = `manifest-multi-${Date.now()}`;
|
|
173
|
+
const screenKey = `${site}/screen`;
|
|
174
|
+
const statusKey = `${site}/status`;
|
|
175
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-'));
|
|
176
|
+
tempDirs.push(dir);
|
|
177
|
+
const file = path.join(dir, `${site}.ts`);
|
|
178
|
+
fs.writeFileSync(file, `export const screen = cli({ site: '${site}', name: 'screen' });`);
|
|
179
|
+
|
|
180
|
+
const entries = await loadTsManifestEntries(file, site, async () => ({
|
|
181
|
+
screen: cli({
|
|
182
|
+
site,
|
|
183
|
+
name: 'screen',
|
|
184
|
+
description: 'capture screen',
|
|
185
|
+
}),
|
|
186
|
+
status: cli({
|
|
187
|
+
site,
|
|
188
|
+
name: 'status',
|
|
189
|
+
description: 'show status',
|
|
190
|
+
}),
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
expect(entries.map(entry => entry.name)).toEqual(['screen', 'status']);
|
|
194
|
+
|
|
195
|
+
getRegistry().delete(screenKey);
|
|
196
|
+
getRegistry().delete(statusKey);
|
|
168
197
|
});
|
|
169
198
|
});
|
package/src/build-manifest.ts
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from 'node:path';
|
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
15
|
import yaml from 'js-yaml';
|
|
16
16
|
import { getErrorMessage } from './errors.js';
|
|
17
|
+
import { fullName, getRegistry, type CliCommand } from './registry.js';
|
|
17
18
|
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const CLIS_DIR = path.resolve(__dirname, 'clis');
|
|
@@ -52,116 +53,50 @@ import { type YamlCliDefinition, parseYamlArgs } from './yaml-schema.js';
|
|
|
52
53
|
|
|
53
54
|
import { isRecord } from './utils.js';
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const ch = source[i];
|
|
68
|
-
|
|
69
|
-
if (quote) {
|
|
70
|
-
if (escaped) {
|
|
71
|
-
escaped = false;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (ch === '\\') {
|
|
75
|
-
escaped = true;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (ch === quote) quote = null;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
83
|
-
quote = ch;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (ch === openChar) {
|
|
88
|
-
depth++;
|
|
89
|
-
} else if (ch === closeChar) {
|
|
90
|
-
depth--;
|
|
91
|
-
if (depth === 0) {
|
|
92
|
-
return source.slice(startIndex + 1, i);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return null;
|
|
56
|
+
const CLI_MODULE_PATTERN = /\bcli\s*\(/;
|
|
57
|
+
|
|
58
|
+
function toManifestArgs(args: CliCommand['args']): ManifestEntry['args'] {
|
|
59
|
+
return args.map(arg => ({
|
|
60
|
+
name: arg.name,
|
|
61
|
+
type: arg.type ?? 'str',
|
|
62
|
+
default: arg.default,
|
|
63
|
+
required: !!arg.required,
|
|
64
|
+
positional: arg.positional || undefined,
|
|
65
|
+
help: arg.help ?? '',
|
|
66
|
+
choices: arg.choices,
|
|
67
|
+
}));
|
|
98
68
|
}
|
|
99
69
|
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const bracketIndex = source.indexOf('[', argsMatch.index);
|
|
105
|
-
if (bracketIndex === -1) return null;
|
|
106
|
-
|
|
107
|
-
return extractBalancedBlock(source, bracketIndex, '[', ']');
|
|
70
|
+
function toTsModulePath(filePath: string, site: string): string {
|
|
71
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
72
|
+
return `${site}/${baseName}.js`;
|
|
108
73
|
}
|
|
109
74
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
.map(s => s.trim().replace(/^['"`]|['"`]$/g, ''))
|
|
117
|
-
.filter(Boolean);
|
|
118
|
-
|
|
119
|
-
return values.length > 0 ? values : undefined;
|
|
75
|
+
function isCliCommandValue(value: unknown, site: string): value is CliCommand {
|
|
76
|
+
return isRecord(value)
|
|
77
|
+
&& typeof value.site === 'string'
|
|
78
|
+
&& value.site === site
|
|
79
|
+
&& typeof value.name === 'string'
|
|
80
|
+
&& Array.isArray(value.args);
|
|
120
81
|
}
|
|
121
82
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let defaultVal: unknown = undefined;
|
|
141
|
-
if (defaultMatch) {
|
|
142
|
-
const raw = defaultMatch[1].trim();
|
|
143
|
-
if (raw === 'true') defaultVal = true;
|
|
144
|
-
else if (raw === 'false') defaultVal = false;
|
|
145
|
-
else if (/^\d+$/.test(raw)) defaultVal = parseInt(raw, 10);
|
|
146
|
-
else if (/^\d+\.\d+$/.test(raw)) defaultVal = parseFloat(raw);
|
|
147
|
-
else defaultVal = raw.replace(/^['"`]|['"`]$/g, '');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
args.push({
|
|
151
|
-
name: nameMatch[1],
|
|
152
|
-
type: typeMatch?.[1] ?? 'str',
|
|
153
|
-
default: defaultVal,
|
|
154
|
-
required: requiredMatch?.[1] === 'true',
|
|
155
|
-
positional: positionalMatch?.[1] === 'true' || undefined,
|
|
156
|
-
help: helpMatch?.[1] ?? '',
|
|
157
|
-
choices: parseInlineChoices(body),
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
cursor = objectStart + body.length;
|
|
161
|
-
if (cursor <= objectStart) break; // safety: prevent infinite loop
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return args;
|
|
83
|
+
function toManifestEntry(cmd: CliCommand, modulePath: string): ManifestEntry {
|
|
84
|
+
return {
|
|
85
|
+
site: cmd.site,
|
|
86
|
+
name: cmd.name,
|
|
87
|
+
description: cmd.description ?? '',
|
|
88
|
+
domain: cmd.domain,
|
|
89
|
+
strategy: (cmd.strategy ?? 'public').toString().toLowerCase(),
|
|
90
|
+
browser: cmd.browser ?? true,
|
|
91
|
+
args: toManifestArgs(cmd.args),
|
|
92
|
+
columns: cmd.columns,
|
|
93
|
+
timeout: cmd.timeoutSeconds,
|
|
94
|
+
deprecated: cmd.deprecated,
|
|
95
|
+
replacedBy: cmd.replacedBy,
|
|
96
|
+
type: 'ts',
|
|
97
|
+
modulePath,
|
|
98
|
+
navigateBefore: cmd.navigateBefore,
|
|
99
|
+
};
|
|
165
100
|
}
|
|
166
101
|
|
|
167
102
|
function scanYaml(filePath: string, site: string): ManifestEntry | null {
|
|
@@ -199,83 +134,49 @@ function scanYaml(filePath: string, site: string): ManifestEntry | null {
|
|
|
199
134
|
}
|
|
200
135
|
}
|
|
201
136
|
|
|
202
|
-
export function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
137
|
+
export async function loadTsManifestEntries(
|
|
138
|
+
filePath: string,
|
|
139
|
+
site: string,
|
|
140
|
+
importer: (moduleHref: string) => Promise<unknown> = moduleHref => import(moduleHref),
|
|
141
|
+
): Promise<ManifestEntry[]> {
|
|
208
142
|
try {
|
|
209
143
|
const src = fs.readFileSync(filePath, 'utf-8');
|
|
210
144
|
|
|
211
145
|
// Helper/test modules should not appear as CLI commands in the manifest.
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Extract columns
|
|
243
|
-
const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
|
|
244
|
-
if (colMatch) {
|
|
245
|
-
entry.columns = colMatch[1].split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Extract args array items: { name: '...', ... }
|
|
249
|
-
const argsBlock = extractTsArgsBlock(src);
|
|
250
|
-
if (argsBlock) {
|
|
251
|
-
entry.args = parseTsArgsBlock(argsBlock);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Extract navigateBefore: false / true / 'https://...'
|
|
255
|
-
const navBoolMatch = src.match(/navigateBefore\s*:\s*(true|false)/);
|
|
256
|
-
if (navBoolMatch) {
|
|
257
|
-
entry.navigateBefore = navBoolMatch[1] === 'true';
|
|
258
|
-
} else {
|
|
259
|
-
const navStringMatch = src.match(/navigateBefore\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
260
|
-
if (navStringMatch) entry.navigateBefore = navStringMatch[1];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const deprecatedBoolMatch = src.match(/deprecated\s*:\s*(true|false)/);
|
|
264
|
-
if (deprecatedBoolMatch) {
|
|
265
|
-
entry.deprecated = deprecatedBoolMatch[1] === 'true';
|
|
266
|
-
} else {
|
|
267
|
-
const deprecatedStringMatch = src.match(/deprecated\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
268
|
-
if (deprecatedStringMatch) entry.deprecated = deprecatedStringMatch[1];
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const replacedByMatch = src.match(/replacedBy\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
272
|
-
if (replacedByMatch) entry.replacedBy = replacedByMatch[1];
|
|
273
|
-
|
|
274
|
-
return entry;
|
|
146
|
+
if (!CLI_MODULE_PATTERN.test(src)) return [];
|
|
147
|
+
|
|
148
|
+
const modulePath = toTsModulePath(filePath, site);
|
|
149
|
+
const registry = getRegistry();
|
|
150
|
+
const before = new Map(registry.entries());
|
|
151
|
+
const mod = await importer(pathToFileURL(filePath).href);
|
|
152
|
+
|
|
153
|
+
const exportedCommands = Object.values(isRecord(mod) ? mod : {})
|
|
154
|
+
.filter(value => isCliCommandValue(value, site));
|
|
155
|
+
|
|
156
|
+
const runtimeCommands = exportedCommands.length > 0
|
|
157
|
+
? exportedCommands
|
|
158
|
+
: [...registry.entries()]
|
|
159
|
+
.filter(([key, cmd]) => {
|
|
160
|
+
if (cmd.site !== site) return false;
|
|
161
|
+
const previous = before.get(key);
|
|
162
|
+
return !previous || previous !== cmd;
|
|
163
|
+
})
|
|
164
|
+
.map(([, cmd]) => cmd);
|
|
165
|
+
|
|
166
|
+
const seen = new Set<string>();
|
|
167
|
+
return runtimeCommands
|
|
168
|
+
.filter((cmd) => {
|
|
169
|
+
const key = fullName(cmd);
|
|
170
|
+
if (seen.has(key)) return false;
|
|
171
|
+
seen.add(key);
|
|
172
|
+
return true;
|
|
173
|
+
})
|
|
174
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
175
|
+
.map(cmd => toManifestEntry(cmd, modulePath));
|
|
275
176
|
} catch (err) {
|
|
276
177
|
// If parsing fails, log a warning (matching scanYaml behaviour) and skip the entry.
|
|
277
178
|
process.stderr.write(`Warning: failed to scan ${filePath}: ${getErrorMessage(err)}\n`);
|
|
278
|
-
return
|
|
179
|
+
return [];
|
|
279
180
|
}
|
|
280
181
|
}
|
|
281
182
|
|
|
@@ -288,7 +189,7 @@ export function shouldReplaceManifestEntry(current: ManifestEntry, next: Manifes
|
|
|
288
189
|
return current.type === 'yaml' && next.type === 'ts';
|
|
289
190
|
}
|
|
290
191
|
|
|
291
|
-
export function buildManifest(): ManifestEntry[] {
|
|
192
|
+
export async function buildManifest(): Promise<ManifestEntry[]> {
|
|
292
193
|
const manifest = new Map<string, ManifestEntry>();
|
|
293
194
|
|
|
294
195
|
if (fs.existsSync(CLIS_DIR)) {
|
|
@@ -313,8 +214,8 @@ export function buildManifest(): ManifestEntry[] {
|
|
|
313
214
|
(file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts') && file !== 'index.ts') ||
|
|
314
215
|
(file.endsWith('.js') && !file.endsWith('.d.js') && !file.endsWith('.test.js') && file !== 'index.js')
|
|
315
216
|
) {
|
|
316
|
-
const
|
|
317
|
-
|
|
217
|
+
const entries = await loadTsManifestEntries(filePath, site);
|
|
218
|
+
for (const entry of entries) {
|
|
318
219
|
const key = `${entry.site}/${entry.name}`;
|
|
319
220
|
const existing = manifest.get(key);
|
|
320
221
|
if (!existing || shouldReplaceManifestEntry(existing, entry)) {
|
|
@@ -332,8 +233,8 @@ export function buildManifest(): ManifestEntry[] {
|
|
|
332
233
|
return [...manifest.values()];
|
|
333
234
|
}
|
|
334
235
|
|
|
335
|
-
function main(): void {
|
|
336
|
-
const manifest = buildManifest();
|
|
236
|
+
async function main(): Promise<void> {
|
|
237
|
+
const manifest = await buildManifest();
|
|
337
238
|
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
|
338
239
|
fs.writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2));
|
|
339
240
|
|
|
@@ -367,5 +268,5 @@ function main(): void {
|
|
|
367
268
|
|
|
368
269
|
const entrypoint = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null;
|
|
369
270
|
if (entrypoint === import.meta.url) {
|
|
370
|
-
main();
|
|
271
|
+
void main();
|
|
371
272
|
}
|
package/src/clis/36kr/hot.ts
CHANGED
package/src/clis/36kr/search.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for CLI adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Clamp a numeric value to [min, max].
|
|
7
|
+
* Matches the signature of lodash.clamp and Rust's clamp.
|
|
8
|
+
*/
|
|
9
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
10
|
+
return Math.max(min, Math.min(value, max));
|
|
11
|
+
}
|
|
@@ -23,7 +23,7 @@ cli({
|
|
|
23
23
|
|
|
24
24
|
// Navigate and wait for the page to hydrate before extracting story data.
|
|
25
25
|
await page.goto(url);
|
|
26
|
-
await page.wait(5);
|
|
26
|
+
await page.wait({ selector: 'article', timeout: 5 });
|
|
27
27
|
|
|
28
28
|
const loadStory = async () => page.evaluate(`(() => {
|
|
29
29
|
const isRobot = /Are you a robot/i.test(document.title)
|