@jackwener/opencli 0.6.3 → 0.7.2
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/LICENSE +190 -28
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/SKILL.md +22 -6
- package/dist/browser.js +2 -3
- package/dist/build-manifest.js +2 -0
- package/dist/cli-manifest.json +604 -24
- package/dist/clis/reddit/comment.d.ts +1 -0
- package/dist/clis/reddit/comment.js +57 -0
- package/dist/clis/reddit/popular.yaml +40 -0
- package/dist/clis/reddit/read.yaml +76 -0
- package/dist/clis/reddit/save.d.ts +1 -0
- package/dist/clis/reddit/save.js +51 -0
- package/dist/clis/reddit/saved.d.ts +1 -0
- package/dist/clis/reddit/saved.js +46 -0
- package/dist/clis/reddit/search.yaml +37 -11
- package/dist/clis/reddit/subreddit.yaml +14 -4
- package/dist/clis/reddit/subscribe.d.ts +1 -0
- package/dist/clis/reddit/subscribe.js +50 -0
- package/dist/clis/reddit/upvote.d.ts +1 -0
- package/dist/clis/reddit/upvote.js +64 -0
- package/dist/clis/reddit/upvoted.d.ts +1 -0
- package/dist/clis/reddit/upvoted.js +46 -0
- package/dist/clis/reddit/user-comments.yaml +45 -0
- package/dist/clis/reddit/user-posts.yaml +43 -0
- package/dist/clis/reddit/user.yaml +39 -0
- package/dist/clis/twitter/article.d.ts +1 -0
- package/dist/clis/twitter/article.js +157 -0
- package/dist/clis/twitter/bookmark.d.ts +1 -0
- package/dist/clis/twitter/bookmark.js +63 -0
- package/dist/clis/twitter/follow.d.ts +1 -0
- package/dist/clis/twitter/follow.js +65 -0
- package/dist/clis/twitter/profile.js +110 -42
- package/dist/clis/twitter/thread.d.ts +1 -0
- package/dist/clis/twitter/thread.js +150 -0
- package/dist/clis/twitter/unbookmark.d.ts +1 -0
- package/dist/clis/twitter/unbookmark.js +62 -0
- package/dist/clis/twitter/unfollow.d.ts +1 -0
- package/dist/clis/twitter/unfollow.js +71 -0
- package/dist/engine.js +2 -1
- package/dist/main.js +41 -10
- package/dist/output.js +2 -1
- package/dist/registry.d.ts +2 -8
- package/dist/snapshotFormatter.d.ts +9 -0
- package/dist/snapshotFormatter.js +352 -15
- package/dist/snapshotFormatter.test.d.ts +7 -0
- package/dist/snapshotFormatter.test.js +521 -0
- package/dist/validate.d.ts +14 -2
- package/dist/verify.d.ts +14 -2
- package/package.json +2 -2
- package/src/browser.ts +2 -4
- package/src/build-manifest.ts +3 -0
- package/src/clis/reddit/comment.ts +60 -0
- package/src/clis/reddit/popular.yaml +40 -0
- package/src/clis/reddit/read.yaml +76 -0
- package/src/clis/reddit/save.ts +54 -0
- package/src/clis/reddit/saved.ts +48 -0
- package/src/clis/reddit/search.yaml +37 -11
- package/src/clis/reddit/subreddit.yaml +14 -4
- package/src/clis/reddit/subscribe.ts +53 -0
- package/src/clis/reddit/upvote.ts +67 -0
- package/src/clis/reddit/upvoted.ts +48 -0
- package/src/clis/reddit/user-comments.yaml +45 -0
- package/src/clis/reddit/user-posts.yaml +43 -0
- package/src/clis/reddit/user.yaml +39 -0
- package/src/clis/twitter/article.ts +161 -0
- package/src/clis/twitter/bookmark.ts +67 -0
- package/src/clis/twitter/follow.ts +69 -0
- package/src/clis/twitter/profile.ts +113 -45
- package/src/clis/twitter/thread.ts +181 -0
- package/src/clis/twitter/unbookmark.ts +66 -0
- package/src/clis/twitter/unfollow.ts +75 -0
- package/src/engine.ts +4 -1
- package/src/main.ts +34 -7
- package/src/output.ts +2 -1
- package/src/registry.ts +2 -8
- package/src/snapshotFormatter.test.ts +579 -0
- package/src/snapshotFormatter.ts +399 -13
- package/src/validate.ts +19 -4
- package/src/verify.ts +17 -3
- package/vitest.config.ts +15 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'twitter',
|
|
6
|
+
name: 'unbookmark',
|
|
7
|
+
description: 'Remove a tweet from bookmarks',
|
|
8
|
+
domain: 'x.com',
|
|
9
|
+
strategy: Strategy.UI,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'url', type: 'string', positional: true, required: true, help: 'Tweet URL to unbookmark' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['status', 'message'],
|
|
15
|
+
func: async (page: IPage | null, kwargs: any) => {
|
|
16
|
+
if (!page) throw new Error('Requires browser');
|
|
17
|
+
|
|
18
|
+
await page.goto(kwargs.url);
|
|
19
|
+
await page.wait(5);
|
|
20
|
+
|
|
21
|
+
const result = await page.evaluate(`(async () => {
|
|
22
|
+
try {
|
|
23
|
+
let attempts = 0;
|
|
24
|
+
let removeBtn = null;
|
|
25
|
+
|
|
26
|
+
while (attempts < 20) {
|
|
27
|
+
// Check if not bookmarked
|
|
28
|
+
const bookmarkBtn = document.querySelector('[data-testid="bookmark"]');
|
|
29
|
+
if (bookmarkBtn) {
|
|
30
|
+
return { ok: true, message: 'Tweet is not bookmarked (already removed).' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
removeBtn = document.querySelector('[data-testid="removeBookmark"]');
|
|
34
|
+
if (removeBtn) break;
|
|
35
|
+
|
|
36
|
+
await new Promise(r => setTimeout(r, 500));
|
|
37
|
+
attempts++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!removeBtn) {
|
|
41
|
+
return { ok: false, message: 'Could not find Remove Bookmark button. Are you logged in?' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
removeBtn.click();
|
|
45
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
46
|
+
|
|
47
|
+
// Verify
|
|
48
|
+
const verify = document.querySelector('[data-testid="bookmark"]');
|
|
49
|
+
if (verify) {
|
|
50
|
+
return { ok: true, message: 'Tweet successfully removed from bookmarks.' };
|
|
51
|
+
} else {
|
|
52
|
+
return { ok: false, message: 'Unbookmark action initiated but UI did not update.' };
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return { ok: false, message: e.toString() };
|
|
56
|
+
}
|
|
57
|
+
})()`);
|
|
58
|
+
|
|
59
|
+
if (result.ok) await page.wait(2);
|
|
60
|
+
|
|
61
|
+
return [{
|
|
62
|
+
status: result.ok ? 'success' : 'failed',
|
|
63
|
+
message: result.message
|
|
64
|
+
}];
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'twitter',
|
|
6
|
+
name: 'unfollow',
|
|
7
|
+
description: 'Unfollow a Twitter user',
|
|
8
|
+
domain: 'x.com',
|
|
9
|
+
strategy: Strategy.UI,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'username', type: 'string', positional: true, required: true, help: 'Twitter screen name (without @)' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['status', 'message'],
|
|
15
|
+
func: async (page: IPage | null, kwargs: any) => {
|
|
16
|
+
if (!page) throw new Error('Requires browser');
|
|
17
|
+
const username = kwargs.username.replace(/^@/, '');
|
|
18
|
+
|
|
19
|
+
await page.goto(`https://x.com/${username}`);
|
|
20
|
+
await page.wait(5);
|
|
21
|
+
|
|
22
|
+
const result = await page.evaluate(`(async () => {
|
|
23
|
+
try {
|
|
24
|
+
let attempts = 0;
|
|
25
|
+
let unfollowBtn = null;
|
|
26
|
+
|
|
27
|
+
while (attempts < 20) {
|
|
28
|
+
// Check if already not following
|
|
29
|
+
const followBtn = document.querySelector('[data-testid$="-follow"]');
|
|
30
|
+
if (followBtn) {
|
|
31
|
+
return { ok: true, message: 'Not following @${username} (already unfollowed).' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
unfollowBtn = document.querySelector('[data-testid$="-unfollow"]');
|
|
35
|
+
if (unfollowBtn) break;
|
|
36
|
+
|
|
37
|
+
await new Promise(r => setTimeout(r, 500));
|
|
38
|
+
attempts++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!unfollowBtn) {
|
|
42
|
+
return { ok: false, message: 'Could not find Unfollow button. Are you logged in?' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Click the unfollow button — this opens a confirmation dialog
|
|
46
|
+
unfollowBtn.click();
|
|
47
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
48
|
+
|
|
49
|
+
// Confirm the unfollow in the dialog
|
|
50
|
+
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
|
|
51
|
+
if (confirmBtn) {
|
|
52
|
+
confirmBtn.click();
|
|
53
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Verify
|
|
57
|
+
const verify = document.querySelector('[data-testid$="-follow"]');
|
|
58
|
+
if (verify) {
|
|
59
|
+
return { ok: true, message: 'Successfully unfollowed @${username}.' };
|
|
60
|
+
} else {
|
|
61
|
+
return { ok: false, message: 'Unfollow action initiated but UI did not update.' };
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { ok: false, message: e.toString() };
|
|
65
|
+
}
|
|
66
|
+
})()`);
|
|
67
|
+
|
|
68
|
+
if (result.ok) await page.wait(2);
|
|
69
|
+
|
|
70
|
+
return [{
|
|
71
|
+
status: result.ok ? 'success' : 'failed',
|
|
72
|
+
message: result.message
|
|
73
|
+
}];
|
|
74
|
+
}
|
|
75
|
+
});
|
package/src/engine.ts
CHANGED
|
@@ -101,7 +101,10 @@ async function discoverClisFromFs(dir: string): Promise<void> {
|
|
|
101
101
|
const filePath = path.join(siteDir, file);
|
|
102
102
|
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
103
103
|
registerYamlCli(filePath, site);
|
|
104
|
-
} else if (
|
|
104
|
+
} else if (
|
|
105
|
+
(file.endsWith('.js') && !file.endsWith('.d.js')) ||
|
|
106
|
+
(file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))
|
|
107
|
+
) {
|
|
105
108
|
promises.push(
|
|
106
109
|
import(`file://${filePath}`).catch((err: any) => {
|
|
107
110
|
process.stderr.write(`Warning: failed to load module ${filePath}: ${err.message}\n`);
|
package/src/main.ts
CHANGED
|
@@ -62,10 +62,18 @@ program.command('list').description('List all available CLI commands').option('-
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name')
|
|
65
|
-
.action(async (target) => {
|
|
65
|
+
.action(async (target) => {
|
|
66
|
+
const { validateClisWithTarget, renderValidationReport } = await import('./validate.js');
|
|
67
|
+
console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target)));
|
|
68
|
+
});
|
|
66
69
|
|
|
67
70
|
program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false)
|
|
68
|
-
.action(async (target, opts) => {
|
|
71
|
+
.action(async (target, opts) => {
|
|
72
|
+
const { verifyClis, renderVerifyReport } = await import('./verify.js');
|
|
73
|
+
const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
|
|
74
|
+
console.log(renderVerifyReport(r));
|
|
75
|
+
process.exitCode = r.ok ? 0 : 1;
|
|
76
|
+
});
|
|
69
77
|
|
|
70
78
|
program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
|
|
71
79
|
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
|
|
@@ -129,18 +137,37 @@ for (const [, cmd] of registry) {
|
|
|
129
137
|
if (!siteCmd) { siteCmd = program.command(cmd.site).description(`${cmd.site} commands`); siteGroups.set(cmd.site, siteCmd); }
|
|
130
138
|
const subCmd = siteCmd.command(cmd.name).description(cmd.description);
|
|
131
139
|
|
|
140
|
+
// Register positional args first, then named options
|
|
141
|
+
const positionalArgs: typeof cmd.args = [];
|
|
132
142
|
for (const arg of cmd.args) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
if (arg.positional) {
|
|
144
|
+
const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
|
|
145
|
+
subCmd.argument(bracket, arg.help ?? '');
|
|
146
|
+
positionalArgs.push(arg);
|
|
147
|
+
} else {
|
|
148
|
+
const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
149
|
+
if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
|
|
150
|
+
else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
151
|
+
else subCmd.option(flag, arg.help ?? '');
|
|
152
|
+
}
|
|
137
153
|
}
|
|
138
154
|
subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
|
|
139
155
|
|
|
140
|
-
subCmd.action(async (
|
|
156
|
+
subCmd.action(async (...actionArgs: any[]) => {
|
|
157
|
+
// Commander passes positional args first, then options object, then the Command
|
|
158
|
+
const actionOpts = actionArgs[positionalArgs.length] ?? {};
|
|
141
159
|
const startTime = Date.now();
|
|
142
160
|
const kwargs: Record<string, any> = {};
|
|
161
|
+
// Collect positional args
|
|
162
|
+
for (let i = 0; i < positionalArgs.length; i++) {
|
|
163
|
+
const arg = positionalArgs[i];
|
|
164
|
+
const v = actionArgs[i];
|
|
165
|
+
if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
|
|
166
|
+
else if (arg.default != null) kwargs[arg.name] = arg.default;
|
|
167
|
+
}
|
|
168
|
+
// Collect named options
|
|
143
169
|
for (const arg of cmd.args) {
|
|
170
|
+
if (arg.positional) continue;
|
|
144
171
|
const v = actionOpts[arg.name]; if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
|
|
145
172
|
else if (arg.default != null) kwargs[arg.name] = arg.default;
|
|
146
173
|
}
|
package/src/output.ts
CHANGED
|
@@ -82,7 +82,8 @@ function renderCsv(data: any, opts: RenderOptions): void {
|
|
|
82
82
|
for (const row of rows) {
|
|
83
83
|
console.log(columns.map(c => {
|
|
84
84
|
const v = String(row[c] ?? '');
|
|
85
|
-
return v.includes(',') || v.includes('"')
|
|
85
|
+
return v.includes(',') || v.includes('"') || v.includes('\n')
|
|
86
|
+
? `"${v.replace(/"/g, '""')}"` : v;
|
|
86
87
|
}).join(','));
|
|
87
88
|
}
|
|
88
89
|
}
|
package/src/registry.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Arg {
|
|
|
17
17
|
type?: string;
|
|
18
18
|
default?: any;
|
|
19
19
|
required?: boolean;
|
|
20
|
+
positional?: boolean;
|
|
20
21
|
help?: string;
|
|
21
22
|
choices?: string[];
|
|
22
23
|
}
|
|
@@ -41,18 +42,11 @@ export interface InternalCliCommand extends CliCommand {
|
|
|
41
42
|
_lazy?: boolean;
|
|
42
43
|
_modulePath?: string;
|
|
43
44
|
}
|
|
44
|
-
export interface CliOptions {
|
|
45
|
+
export interface CliOptions extends Partial<Omit<CliCommand, 'args' | 'description'>> {
|
|
45
46
|
site: string;
|
|
46
47
|
name: string;
|
|
47
48
|
description?: string;
|
|
48
|
-
domain?: string;
|
|
49
|
-
strategy?: Strategy;
|
|
50
|
-
browser?: boolean;
|
|
51
49
|
args?: Arg[];
|
|
52
|
-
columns?: string[];
|
|
53
|
-
func?: (page: IPage, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
|
|
54
|
-
pipeline?: any[];
|
|
55
|
-
timeoutSeconds?: number;
|
|
56
50
|
}
|
|
57
51
|
const _registry = new Map<string, CliCommand>();
|
|
58
52
|
|