@jackwener/opencli 0.6.2 → 0.7.0
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/SKILL.md +7 -2
- package/dist/build-manifest.js +2 -0
- package/dist/cli-manifest.json +723 -104
- package/dist/clis/boss/detail.d.ts +1 -0
- package/dist/clis/boss/detail.js +104 -0
- package/dist/clis/boss/search.js +2 -1
- 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/main.js +31 -8
- package/dist/registry.d.ts +1 -0
- package/package.json +1 -1
- package/src/build-manifest.ts +3 -0
- package/src/clis/boss/detail.ts +115 -0
- package/src/clis/boss/search.ts +2 -1
- 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/main.ts +24 -5
- package/src/registry.ts +1 -0
|
@@ -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/main.ts
CHANGED
|
@@ -129,18 +129,37 @@ for (const [, cmd] of registry) {
|
|
|
129
129
|
if (!siteCmd) { siteCmd = program.command(cmd.site).description(`${cmd.site} commands`); siteGroups.set(cmd.site, siteCmd); }
|
|
130
130
|
const subCmd = siteCmd.command(cmd.name).description(cmd.description);
|
|
131
131
|
|
|
132
|
+
// Register positional args first, then named options
|
|
133
|
+
const positionalArgs: typeof cmd.args = [];
|
|
132
134
|
for (const arg of cmd.args) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
if (arg.positional) {
|
|
136
|
+
const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
|
|
137
|
+
subCmd.argument(bracket, arg.help ?? '');
|
|
138
|
+
positionalArgs.push(arg);
|
|
139
|
+
} else {
|
|
140
|
+
const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
141
|
+
if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
|
|
142
|
+
else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
143
|
+
else subCmd.option(flag, arg.help ?? '');
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
|
|
139
147
|
|
|
140
|
-
subCmd.action(async (
|
|
148
|
+
subCmd.action(async (...actionArgs: any[]) => {
|
|
149
|
+
// Commander passes positional args first, then options object, then the Command
|
|
150
|
+
const actionOpts = actionArgs[positionalArgs.length] ?? {};
|
|
141
151
|
const startTime = Date.now();
|
|
142
152
|
const kwargs: Record<string, any> = {};
|
|
153
|
+
// Collect positional args
|
|
154
|
+
for (let i = 0; i < positionalArgs.length; i++) {
|
|
155
|
+
const arg = positionalArgs[i];
|
|
156
|
+
const v = actionArgs[i];
|
|
157
|
+
if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
|
|
158
|
+
else if (arg.default != null) kwargs[arg.name] = arg.default;
|
|
159
|
+
}
|
|
160
|
+
// Collect named options
|
|
143
161
|
for (const arg of cmd.args) {
|
|
162
|
+
if (arg.positional) continue;
|
|
144
163
|
const v = actionOpts[arg.name]; if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
|
|
145
164
|
else if (arg.default != null) kwargs[arg.name] = arg.default;
|
|
146
165
|
}
|