@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.
Files changed (68) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/SKILL.md +7 -2
  4. package/dist/build-manifest.js +2 -0
  5. package/dist/cli-manifest.json +723 -104
  6. package/dist/clis/boss/detail.d.ts +1 -0
  7. package/dist/clis/boss/detail.js +104 -0
  8. package/dist/clis/boss/search.js +2 -1
  9. package/dist/clis/reddit/comment.d.ts +1 -0
  10. package/dist/clis/reddit/comment.js +57 -0
  11. package/dist/clis/reddit/popular.yaml +40 -0
  12. package/dist/clis/reddit/read.yaml +76 -0
  13. package/dist/clis/reddit/save.d.ts +1 -0
  14. package/dist/clis/reddit/save.js +51 -0
  15. package/dist/clis/reddit/saved.d.ts +1 -0
  16. package/dist/clis/reddit/saved.js +46 -0
  17. package/dist/clis/reddit/search.yaml +37 -11
  18. package/dist/clis/reddit/subreddit.yaml +14 -4
  19. package/dist/clis/reddit/subscribe.d.ts +1 -0
  20. package/dist/clis/reddit/subscribe.js +50 -0
  21. package/dist/clis/reddit/upvote.d.ts +1 -0
  22. package/dist/clis/reddit/upvote.js +64 -0
  23. package/dist/clis/reddit/upvoted.d.ts +1 -0
  24. package/dist/clis/reddit/upvoted.js +46 -0
  25. package/dist/clis/reddit/user-comments.yaml +45 -0
  26. package/dist/clis/reddit/user-posts.yaml +43 -0
  27. package/dist/clis/reddit/user.yaml +39 -0
  28. package/dist/clis/twitter/article.d.ts +1 -0
  29. package/dist/clis/twitter/article.js +157 -0
  30. package/dist/clis/twitter/bookmark.d.ts +1 -0
  31. package/dist/clis/twitter/bookmark.js +63 -0
  32. package/dist/clis/twitter/follow.d.ts +1 -0
  33. package/dist/clis/twitter/follow.js +65 -0
  34. package/dist/clis/twitter/profile.js +110 -42
  35. package/dist/clis/twitter/thread.d.ts +1 -0
  36. package/dist/clis/twitter/thread.js +150 -0
  37. package/dist/clis/twitter/unbookmark.d.ts +1 -0
  38. package/dist/clis/twitter/unbookmark.js +62 -0
  39. package/dist/clis/twitter/unfollow.d.ts +1 -0
  40. package/dist/clis/twitter/unfollow.js +71 -0
  41. package/dist/main.js +31 -8
  42. package/dist/registry.d.ts +1 -0
  43. package/package.json +1 -1
  44. package/src/build-manifest.ts +3 -0
  45. package/src/clis/boss/detail.ts +115 -0
  46. package/src/clis/boss/search.ts +2 -1
  47. package/src/clis/reddit/comment.ts +60 -0
  48. package/src/clis/reddit/popular.yaml +40 -0
  49. package/src/clis/reddit/read.yaml +76 -0
  50. package/src/clis/reddit/save.ts +54 -0
  51. package/src/clis/reddit/saved.ts +48 -0
  52. package/src/clis/reddit/search.yaml +37 -11
  53. package/src/clis/reddit/subreddit.yaml +14 -4
  54. package/src/clis/reddit/subscribe.ts +53 -0
  55. package/src/clis/reddit/upvote.ts +67 -0
  56. package/src/clis/reddit/upvoted.ts +48 -0
  57. package/src/clis/reddit/user-comments.yaml +45 -0
  58. package/src/clis/reddit/user-posts.yaml +43 -0
  59. package/src/clis/reddit/user.yaml +39 -0
  60. package/src/clis/twitter/article.ts +161 -0
  61. package/src/clis/twitter/bookmark.ts +67 -0
  62. package/src/clis/twitter/follow.ts +69 -0
  63. package/src/clis/twitter/profile.ts +113 -45
  64. package/src/clis/twitter/thread.ts +181 -0
  65. package/src/clis/twitter/unbookmark.ts +66 -0
  66. package/src/clis/twitter/unfollow.ts +75 -0
  67. package/src/main.ts +24 -5
  68. 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
- const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
134
- if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
135
- else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
136
- else subCmd.option(flag, arg.help ?? '');
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 (actionOpts) => {
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
  }
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
  }