@jackwener/opencli 1.5.5 → 1.5.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 +27 -2
- package/README.zh-CN.md +36 -4
- package/dist/browser/daemon-client.d.ts +5 -1
- package/dist/browser/page.d.ts +6 -0
- package/dist/browser/page.js +15 -0
- package/dist/cli-manifest.json +1229 -67
- package/dist/clis/band/bands.d.ts +1 -0
- package/dist/clis/band/bands.js +72 -0
- package/dist/clis/band/mentions.d.ts +1 -0
- package/dist/clis/band/mentions.js +127 -0
- package/dist/clis/band/post.d.ts +1 -0
- package/dist/clis/band/post.js +175 -0
- package/dist/clis/band/posts.d.ts +1 -0
- package/dist/clis/band/posts.js +94 -0
- package/dist/clis/doubao/detail.d.ts +1 -0
- package/dist/clis/doubao/detail.js +33 -0
- package/dist/clis/doubao/detail.test.d.ts +1 -0
- package/dist/clis/doubao/detail.test.js +42 -0
- package/dist/clis/doubao/history.d.ts +1 -0
- package/dist/clis/doubao/history.js +28 -0
- package/dist/clis/doubao/history.test.d.ts +1 -0
- package/dist/clis/doubao/history.test.js +37 -0
- package/dist/clis/doubao/meeting-summary.d.ts +1 -0
- package/dist/clis/doubao/meeting-summary.js +39 -0
- package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
- package/dist/clis/doubao/meeting-transcript.js +36 -0
- package/dist/clis/doubao/utils.d.ts +27 -0
- package/dist/clis/doubao/utils.js +317 -0
- package/dist/clis/doubao/utils.test.d.ts +1 -0
- package/dist/clis/doubao/utils.test.js +24 -0
- package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
- package/dist/clis/douyin/_shared/public-api.js +29 -0
- package/dist/clis/douyin/user-videos.d.ts +5 -0
- package/dist/clis/douyin/user-videos.js +74 -0
- package/dist/clis/douyin/user-videos.test.d.ts +1 -0
- package/dist/clis/douyin/user-videos.test.js +108 -0
- package/dist/clis/ones/common.d.ts +32 -0
- package/dist/clis/ones/common.js +144 -0
- package/dist/clis/ones/enrich-tasks.d.ts +5 -0
- package/dist/clis/ones/enrich-tasks.js +37 -0
- package/dist/clis/ones/login.d.ts +1 -0
- package/dist/clis/ones/login.js +80 -0
- package/dist/clis/ones/logout.d.ts +1 -0
- package/dist/clis/ones/logout.js +17 -0
- package/dist/clis/ones/me.d.ts +1 -0
- package/dist/clis/ones/me.js +30 -0
- package/dist/clis/ones/my-tasks.d.ts +1 -0
- package/dist/clis/ones/my-tasks.js +120 -0
- package/dist/clis/ones/resolve-labels.d.ts +10 -0
- package/dist/clis/ones/resolve-labels.js +64 -0
- package/dist/clis/ones/task-helpers.d.ts +29 -0
- package/dist/clis/ones/task-helpers.js +212 -0
- package/dist/clis/ones/task-helpers.test.d.ts +1 -0
- package/dist/clis/ones/task-helpers.test.js +12 -0
- package/dist/clis/ones/task.d.ts +1 -0
- package/dist/clis/ones/task.js +66 -0
- package/dist/clis/ones/tasks.d.ts +1 -0
- package/dist/clis/ones/tasks.js +79 -0
- package/dist/clis/ones/token-info.d.ts +1 -0
- package/dist/clis/ones/token-info.js +42 -0
- package/dist/clis/ones/worklog.d.ts +11 -0
- package/dist/clis/ones/worklog.js +267 -0
- package/dist/clis/ones/worklog.test.d.ts +1 -0
- package/dist/clis/ones/worklog.test.js +20 -0
- package/dist/clis/spotify/spotify.d.ts +1 -0
- package/dist/clis/spotify/spotify.js +316 -0
- package/dist/clis/spotify/utils.d.ts +21 -0
- package/dist/clis/spotify/utils.js +66 -0
- package/dist/clis/spotify/utils.test.d.ts +1 -0
- package/dist/clis/spotify/utils.test.js +67 -0
- package/dist/clis/tieba/commands.test.d.ts +4 -0
- package/dist/clis/tieba/commands.test.js +79 -0
- package/dist/clis/tieba/hot.d.ts +1 -0
- package/dist/clis/tieba/hot.js +48 -0
- package/dist/clis/tieba/posts.d.ts +1 -0
- package/dist/clis/tieba/posts.js +85 -0
- package/dist/clis/tieba/read.d.ts +1 -0
- package/dist/clis/tieba/read.js +140 -0
- package/dist/clis/tieba/search.d.ts +1 -0
- package/dist/clis/tieba/search.js +108 -0
- package/dist/clis/tieba/utils.d.ts +101 -0
- package/dist/clis/tieba/utils.js +240 -0
- package/dist/clis/tieba/utils.test.d.ts +1 -0
- package/dist/clis/tieba/utils.test.js +290 -0
- package/dist/clis/weread/book.js +100 -13
- package/dist/clis/weread/commands.test.js +221 -0
- package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
- package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
- package/dist/clis/weread/search-regression.test.d.ts +1 -0
- package/dist/clis/weread/search-regression.test.js +407 -0
- package/dist/clis/weread/search.js +143 -7
- package/dist/clis/weread/shelf.js +13 -95
- package/dist/clis/weread/utils.d.ts +46 -0
- package/dist/clis/weread/utils.js +214 -7
- package/dist/clis/weread/utils.test.js +71 -1
- package/dist/clis/xiaohongshu/publish.d.ts +1 -1
- package/dist/clis/xiaohongshu/publish.js +78 -31
- package/dist/clis/xiaohongshu/publish.test.js +66 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.js +2 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
- package/dist/clis/xueqiu/comments.d.ts +118 -0
- package/dist/clis/xueqiu/comments.js +354 -0
- package/dist/clis/xueqiu/comments.test.d.ts +1 -0
- package/dist/clis/xueqiu/comments.test.js +696 -0
- package/dist/clis/youtube/transcript.js +2 -4
- package/dist/clis/youtube/utils.d.ts +9 -0
- package/dist/clis/youtube/utils.js +67 -3
- package/dist/clis/youtube/utils.test.d.ts +1 -0
- package/dist/clis/youtube/utils.test.js +37 -0
- package/dist/clis/youtube/video.js +16 -15
- package/dist/clis/zsxq/dynamics.d.ts +1 -0
- package/dist/clis/zsxq/dynamics.js +47 -0
- package/dist/clis/zsxq/groups.d.ts +1 -0
- package/dist/clis/zsxq/groups.js +32 -0
- package/dist/clis/zsxq/search.d.ts +1 -0
- package/dist/clis/zsxq/search.js +43 -0
- package/dist/clis/zsxq/search.test.d.ts +1 -0
- package/dist/clis/zsxq/search.test.js +24 -0
- package/dist/clis/zsxq/topic.d.ts +1 -0
- package/dist/clis/zsxq/topic.js +47 -0
- package/dist/clis/zsxq/topic.test.d.ts +1 -0
- package/dist/clis/zsxq/topic.test.js +29 -0
- package/dist/clis/zsxq/topics.d.ts +1 -0
- package/dist/clis/zsxq/topics.js +25 -0
- package/dist/clis/zsxq/topics.test.d.ts +1 -0
- package/dist/clis/zsxq/topics.test.js +24 -0
- package/dist/clis/zsxq/utils.d.ts +97 -0
- package/dist/clis/zsxq/utils.js +230 -0
- package/dist/commanderAdapter.js +1 -1
- package/dist/commanderAdapter.test.js +39 -0
- package/dist/external-clis.yaml +17 -0
- package/dist/types.d.ts +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/band.md +63 -0
- package/docs/adapters/browser/ones.md +59 -0
- package/docs/adapters/browser/spotify.md +62 -0
- package/docs/adapters/browser/tieba.md +45 -0
- package/docs/adapters/browser/xueqiu.md +5 -0
- package/docs/adapters/browser/zsxq.md +49 -0
- package/docs/adapters/index.md +5 -2
- package/docs/adapters-doc/ones.md +32 -0
- package/extension/src/background.ts +15 -0
- package/extension/src/cdp.ts +42 -0
- package/extension/src/protocol.ts +5 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +16 -0
- package/src/browser/daemon-client.ts +5 -1
- package/src/browser/page.ts +16 -0
- package/src/clis/band/bands.ts +76 -0
- package/src/clis/band/mentions.ts +134 -0
- package/src/clis/band/post.ts +187 -0
- package/src/clis/band/posts.ts +106 -0
- package/src/clis/doubao/detail.test.ts +53 -0
- package/src/clis/doubao/detail.ts +41 -0
- package/src/clis/doubao/history.test.ts +45 -0
- package/src/clis/doubao/history.ts +32 -0
- package/src/clis/doubao/meeting-summary.ts +53 -0
- package/src/clis/doubao/meeting-transcript.ts +48 -0
- package/src/clis/doubao/utils.test.ts +45 -0
- package/src/clis/doubao/utils.ts +371 -0
- package/src/clis/douyin/_shared/public-api.ts +84 -0
- package/src/clis/douyin/user-videos.test.ts +122 -0
- package/src/clis/douyin/user-videos.ts +101 -0
- package/src/clis/ones/common.ts +187 -0
- package/src/clis/ones/enrich-tasks.ts +47 -0
- package/src/clis/ones/login.ts +103 -0
- package/src/clis/ones/logout.ts +19 -0
- package/src/clis/ones/me.ts +34 -0
- package/src/clis/ones/my-tasks.ts +148 -0
- package/src/clis/ones/resolve-labels.ts +80 -0
- package/src/clis/ones/task-helpers.test.ts +14 -0
- package/src/clis/ones/task-helpers.ts +214 -0
- package/src/clis/ones/task.ts +79 -0
- package/src/clis/ones/tasks.ts +92 -0
- package/src/clis/ones/token-info.ts +46 -0
- package/src/clis/ones/worklog.test.ts +24 -0
- package/src/clis/ones/worklog.ts +306 -0
- package/src/clis/spotify/spotify.ts +328 -0
- package/src/clis/spotify/utils.test.ts +87 -0
- package/src/clis/spotify/utils.ts +92 -0
- package/src/clis/tieba/commands.test.ts +86 -0
- package/src/clis/tieba/hot.ts +52 -0
- package/src/clis/tieba/posts.ts +108 -0
- package/src/clis/tieba/read.ts +158 -0
- package/src/clis/tieba/search.ts +119 -0
- package/src/clis/tieba/utils.test.ts +322 -0
- package/src/clis/tieba/utils.ts +348 -0
- package/src/clis/weread/book.ts +116 -13
- package/src/clis/weread/commands.test.ts +249 -0
- package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
- package/src/clis/weread/search-regression.test.ts +440 -0
- package/src/clis/weread/search.ts +189 -9
- package/src/clis/weread/shelf.ts +20 -122
- package/src/clis/weread/utils.test.ts +81 -1
- package/src/clis/weread/utils.ts +264 -7
- package/src/clis/xiaohongshu/publish.test.ts +79 -1
- package/src/clis/xiaohongshu/publish.ts +84 -30
- package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
- package/src/clis/xiaohongshu/user-helpers.ts +4 -0
- package/src/clis/xueqiu/comments.test.ts +823 -0
- package/src/clis/xueqiu/comments.ts +461 -0
- package/src/clis/youtube/transcript.ts +2 -4
- package/src/clis/youtube/utils.test.ts +43 -0
- package/src/clis/youtube/utils.ts +69 -0
- package/src/clis/youtube/video.ts +16 -15
- package/src/clis/zsxq/dynamics.ts +60 -0
- package/src/clis/zsxq/groups.ts +41 -0
- package/src/clis/zsxq/search.test.ts +29 -0
- package/src/clis/zsxq/search.ts +54 -0
- package/src/clis/zsxq/topic.test.ts +34 -0
- package/src/clis/zsxq/topic.ts +68 -0
- package/src/clis/zsxq/topics.test.ts +29 -0
- package/src/clis/zsxq/topics.ts +36 -0
- package/src/clis/zsxq/utils.ts +351 -0
- package/src/commanderAdapter.test.ts +47 -0
- package/src/commanderAdapter.ts +1 -1
- package/src/external-clis.yaml +17 -0
- package/src/types.ts +5 -0
- package/tests/e2e/band-auth.test.ts +20 -0
- package/tests/e2e/browser-auth-helpers.ts +18 -0
- package/tests/e2e/browser-auth.test.ts +35 -47
- package/tests/e2e/browser-public.test.ts +288 -0
- package/tests/e2e/management.test.ts +1 -1
- package/tests/e2e/plugin-management.test.ts +1 -1
- package/vitest.config.ts +1 -0
- package/SKILL.md +0 -879
- package/dist/weread-private-api-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.js +0 -39
- package/src/weread-search-regression.test.ts +0 -44
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { ArgumentError, AuthRequiredError, CliError } from '../../errors.js';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export interface ZsxqUser {
|
|
5
|
+
user_id?: number;
|
|
6
|
+
name?: string;
|
|
7
|
+
avatar_url?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ZsxqGroup {
|
|
11
|
+
group_id?: number;
|
|
12
|
+
name?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
background_url?: string;
|
|
15
|
+
owner?: ZsxqUser;
|
|
16
|
+
statistics?: {
|
|
17
|
+
topics_count?: number;
|
|
18
|
+
answers_count?: number;
|
|
19
|
+
comments_count?: number;
|
|
20
|
+
likes_count?: number;
|
|
21
|
+
subscriptions_count?: number;
|
|
22
|
+
};
|
|
23
|
+
category?: {
|
|
24
|
+
title?: string;
|
|
25
|
+
};
|
|
26
|
+
user_specific?: {
|
|
27
|
+
join_time?: string;
|
|
28
|
+
validity?: {
|
|
29
|
+
end_time?: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ZsxqComment {
|
|
35
|
+
comment_id?: number;
|
|
36
|
+
create_time?: string;
|
|
37
|
+
text?: string;
|
|
38
|
+
owner?: ZsxqUser;
|
|
39
|
+
likes_count?: number;
|
|
40
|
+
rewards_count?: number;
|
|
41
|
+
repliee?: ZsxqUser;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ZsxqTopic {
|
|
45
|
+
topic_id?: number;
|
|
46
|
+
create_time?: string;
|
|
47
|
+
comments_count?: number;
|
|
48
|
+
likes_count?: number;
|
|
49
|
+
readers_count?: number;
|
|
50
|
+
reading_count?: number;
|
|
51
|
+
rewards_count?: number;
|
|
52
|
+
title?: string;
|
|
53
|
+
type?: string;
|
|
54
|
+
group?: ZsxqGroup;
|
|
55
|
+
owner?: ZsxqUser;
|
|
56
|
+
user_specific?: Record<string, unknown>;
|
|
57
|
+
talk?: {
|
|
58
|
+
owner?: ZsxqUser;
|
|
59
|
+
text?: string;
|
|
60
|
+
};
|
|
61
|
+
question?: {
|
|
62
|
+
owner?: ZsxqUser;
|
|
63
|
+
text?: string;
|
|
64
|
+
};
|
|
65
|
+
answer?: {
|
|
66
|
+
owner?: ZsxqUser;
|
|
67
|
+
text?: string;
|
|
68
|
+
};
|
|
69
|
+
task?: {
|
|
70
|
+
owner?: ZsxqUser;
|
|
71
|
+
text?: string;
|
|
72
|
+
};
|
|
73
|
+
solution?: {
|
|
74
|
+
owner?: ZsxqUser;
|
|
75
|
+
text?: string;
|
|
76
|
+
};
|
|
77
|
+
show_comments?: ZsxqComment[];
|
|
78
|
+
comments?: ZsxqComment[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface BrowserFetchResult {
|
|
82
|
+
ok: boolean;
|
|
83
|
+
url?: string;
|
|
84
|
+
status?: number;
|
|
85
|
+
error?: string;
|
|
86
|
+
data?: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const SITE_DOMAIN = 'wx.zsxq.com';
|
|
90
|
+
const SITE_URL = 'https://wx.zsxq.com';
|
|
91
|
+
|
|
92
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
93
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
94
|
+
? value as Record<string, unknown>
|
|
95
|
+
: null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pickArray<T>(...values: unknown[]): T[] {
|
|
99
|
+
for (const value of values) {
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value as T[];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function ensureZsxqPage(page: IPage): Promise<void> {
|
|
108
|
+
await page.goto(SITE_URL);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function ensureZsxqAuth(page: IPage): Promise<void> {
|
|
112
|
+
// zsxq uses httpOnly cookies that may be on different subdomains.
|
|
113
|
+
// Verify auth by attempting a lightweight API call instead of checking cookies.
|
|
114
|
+
try {
|
|
115
|
+
const result = await page.evaluate(`
|
|
116
|
+
(async () => {
|
|
117
|
+
try {
|
|
118
|
+
const r = await new Promise((resolve, reject) => {
|
|
119
|
+
const xhr = new XMLHttpRequest();
|
|
120
|
+
xhr.open('GET', 'https://api.zsxq.com/v2/groups', true);
|
|
121
|
+
xhr.withCredentials = true;
|
|
122
|
+
xhr.setRequestHeader('accept', 'application/json');
|
|
123
|
+
xhr.onload = () => {
|
|
124
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
125
|
+
try { resolve(JSON.parse(xhr.responseText)); }
|
|
126
|
+
catch { resolve(null); }
|
|
127
|
+
} else { resolve(null); }
|
|
128
|
+
};
|
|
129
|
+
xhr.onerror = () => resolve(null);
|
|
130
|
+
xhr.send();
|
|
131
|
+
});
|
|
132
|
+
return r !== null;
|
|
133
|
+
} catch { return false; }
|
|
134
|
+
})()
|
|
135
|
+
`);
|
|
136
|
+
if (!result) {
|
|
137
|
+
throw new AuthRequiredError('zsxq.com');
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
141
|
+
throw new AuthRequiredError('zsxq.com');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function getCookieValue(page: IPage, name: string): Promise<string | undefined> {
|
|
146
|
+
const cookies = await page.getCookies({ domain: SITE_DOMAIN });
|
|
147
|
+
return cookies.find(cookie => cookie.name === name)?.value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function getActiveGroupId(page: IPage): Promise<string> {
|
|
151
|
+
const groupId = await page.evaluate(`
|
|
152
|
+
(() => {
|
|
153
|
+
const target = localStorage.getItem('target_group');
|
|
154
|
+
if (target) {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(target);
|
|
157
|
+
if (parsed.group_id) return String(parsed.group_id);
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
})()
|
|
162
|
+
`);
|
|
163
|
+
if (groupId) return groupId;
|
|
164
|
+
|
|
165
|
+
throw new ArgumentError(
|
|
166
|
+
'Cannot determine active group_id',
|
|
167
|
+
'Pass --group_id <id> or open the target 知识星球 page in Chrome first',
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function browserJsonRequest(page: IPage, path: string): Promise<BrowserFetchResult> {
|
|
172
|
+
return await page.evaluate(`
|
|
173
|
+
(async () => {
|
|
174
|
+
const path = ${JSON.stringify(path)};
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
return await new Promise((resolve) => {
|
|
178
|
+
const xhr = new XMLHttpRequest();
|
|
179
|
+
xhr.open('GET', path, true);
|
|
180
|
+
xhr.withCredentials = true;
|
|
181
|
+
xhr.setRequestHeader('accept', 'application/json, text/plain, */*');
|
|
182
|
+
xhr.onload = () => {
|
|
183
|
+
let parsed = null;
|
|
184
|
+
if (xhr.responseText) {
|
|
185
|
+
try { parsed = JSON.parse(xhr.responseText); }
|
|
186
|
+
catch {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
resolve({
|
|
190
|
+
ok: xhr.status >= 200 && xhr.status < 300,
|
|
191
|
+
url: path,
|
|
192
|
+
status: xhr.status,
|
|
193
|
+
data: parsed,
|
|
194
|
+
error: xhr.status >= 200 && xhr.status < 300 ? undefined : 'HTTP ' + xhr.status,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
xhr.onerror = () => resolve({
|
|
198
|
+
ok: false,
|
|
199
|
+
url: path,
|
|
200
|
+
error: 'Network error',
|
|
201
|
+
});
|
|
202
|
+
xhr.send();
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
url: path,
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
})()
|
|
212
|
+
`) as BrowserFetchResult;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function fetchFirstJson(page: IPage, paths: string[]): Promise<BrowserFetchResult> {
|
|
216
|
+
let lastFailure: BrowserFetchResult | null = null;
|
|
217
|
+
|
|
218
|
+
for (const path of paths) {
|
|
219
|
+
const result = await browserJsonRequest(page, path);
|
|
220
|
+
if (result.ok) {
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
lastFailure = result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!lastFailure) {
|
|
227
|
+
throw new CliError(
|
|
228
|
+
'FETCH_ERROR',
|
|
229
|
+
'No candidate endpoint returned JSON',
|
|
230
|
+
`Checked endpoints: ${paths.join(', ')}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
throw new CliError(
|
|
235
|
+
'FETCH_ERROR',
|
|
236
|
+
lastFailure.error || 'Failed to fetch ZSXQ API',
|
|
237
|
+
`Checked endpoints: ${paths.join(', ')}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function unwrapRespData<T>(payload: unknown): T {
|
|
242
|
+
const record = asRecord(payload);
|
|
243
|
+
if (!record) {
|
|
244
|
+
throw new CliError('PARSE_ERROR', 'Invalid ZSXQ API response');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (record.succeeded === false) {
|
|
248
|
+
const code = typeof record.code === 'number' ? String(record.code) : 'API_ERROR';
|
|
249
|
+
const message = typeof record.info === 'string'
|
|
250
|
+
? record.info
|
|
251
|
+
: typeof record.error === 'string'
|
|
252
|
+
? record.error
|
|
253
|
+
: 'ZSXQ API returned an error';
|
|
254
|
+
throw new CliError(code, message);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (record.resp_data ?? record.data ?? payload) as T;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function getTopicsFromResponse(payload: unknown): ZsxqTopic[] {
|
|
261
|
+
const data = unwrapRespData<Record<string, unknown> | ZsxqTopic[]>(payload);
|
|
262
|
+
if (Array.isArray(data)) return data;
|
|
263
|
+
return pickArray<ZsxqTopic>(
|
|
264
|
+
data.topics,
|
|
265
|
+
data.list,
|
|
266
|
+
data.records,
|
|
267
|
+
data.items,
|
|
268
|
+
data.search_result,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function getCommentsFromResponse(payload: unknown): ZsxqComment[] {
|
|
273
|
+
const data = unwrapRespData<Record<string, unknown> | ZsxqComment[]>(payload);
|
|
274
|
+
if (Array.isArray(data)) return data;
|
|
275
|
+
return pickArray<ZsxqComment>(data.comments, data.list, data.items);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function getGroupsFromResponse(payload: unknown): ZsxqGroup[] {
|
|
279
|
+
const data = unwrapRespData<Record<string, unknown> | ZsxqGroup[]>(payload);
|
|
280
|
+
if (Array.isArray(data)) return data;
|
|
281
|
+
return pickArray<ZsxqGroup>(data.groups, data.list, data.items);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function getTopicFromResponse(payload: unknown): ZsxqTopic | null {
|
|
285
|
+
const data = unwrapRespData<Record<string, unknown> | ZsxqTopic>(payload);
|
|
286
|
+
if (Array.isArray(data)) return data[0] ?? null;
|
|
287
|
+
if (typeof data.topic_id === 'number') return data;
|
|
288
|
+
const record = asRecord(data);
|
|
289
|
+
if (!record) return null;
|
|
290
|
+
const topic = record.topic;
|
|
291
|
+
return topic && typeof topic === 'object' ? topic as ZsxqTopic : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getTopicAuthor(topic: ZsxqTopic): string {
|
|
295
|
+
return (
|
|
296
|
+
topic.owner?.name ||
|
|
297
|
+
topic.talk?.owner?.name ||
|
|
298
|
+
topic.question?.owner?.name ||
|
|
299
|
+
topic.answer?.owner?.name ||
|
|
300
|
+
topic.task?.owner?.name ||
|
|
301
|
+
topic.solution?.owner?.name ||
|
|
302
|
+
''
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function getTopicText(topic: ZsxqTopic): string {
|
|
307
|
+
const primary = [
|
|
308
|
+
topic.title,
|
|
309
|
+
topic.talk?.text,
|
|
310
|
+
topic.question?.text,
|
|
311
|
+
topic.answer?.text,
|
|
312
|
+
topic.task?.text,
|
|
313
|
+
topic.solution?.text,
|
|
314
|
+
].find(value => typeof value === 'string' && value.trim());
|
|
315
|
+
return (primary || '').replace(/\s+/g, ' ').trim();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function getTopicUrl(topicId: number | string | undefined): string {
|
|
319
|
+
return topicId ? `${SITE_URL}/topic/${topicId}` : SITE_URL;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function summarizeComments(comments: ZsxqComment[], limit: number = 3): string {
|
|
323
|
+
return comments
|
|
324
|
+
.slice(0, limit)
|
|
325
|
+
.map((comment) => {
|
|
326
|
+
const author = comment.owner?.name || '匿名';
|
|
327
|
+
const target = comment.repliee?.name ? ` -> ${comment.repliee.name}` : '';
|
|
328
|
+
const text = (comment.text || '').replace(/\s+/g, ' ').trim();
|
|
329
|
+
return `${author}${target}: ${text}`;
|
|
330
|
+
})
|
|
331
|
+
.join(' | ');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function toTopicRow(topic: ZsxqTopic): Record<string, unknown> {
|
|
335
|
+
const topicId = topic.topic_id ?? '';
|
|
336
|
+
const comments = pickArray<ZsxqComment>(topic.show_comments, topic.comments);
|
|
337
|
+
return {
|
|
338
|
+
topic_id: topicId,
|
|
339
|
+
type: topic.type || '',
|
|
340
|
+
group: topic.group?.name || '',
|
|
341
|
+
author: getTopicAuthor(topic),
|
|
342
|
+
title: getTopicText(topic).slice(0, 120),
|
|
343
|
+
content: getTopicText(topic),
|
|
344
|
+
comments: topic.comments_count ?? comments.length ?? 0,
|
|
345
|
+
likes: topic.likes_count ?? 0,
|
|
346
|
+
readers: topic.readers_count ?? topic.reading_count ?? 0,
|
|
347
|
+
time: topic.create_time || '',
|
|
348
|
+
comment_preview: summarizeComments(comments),
|
|
349
|
+
url: getTopicUrl(topicId),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
@@ -76,3 +76,50 @@ describe('commanderAdapter arg passing', () => {
|
|
|
76
76
|
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
describe('commanderAdapter boolean alias support', () => {
|
|
81
|
+
const cmd: CliCommand = {
|
|
82
|
+
site: 'reddit',
|
|
83
|
+
name: 'save',
|
|
84
|
+
description: 'Save a post',
|
|
85
|
+
browser: false,
|
|
86
|
+
args: [
|
|
87
|
+
{ name: 'post-id', positional: true, required: true, help: 'Post ID' },
|
|
88
|
+
{ name: 'undo', type: 'boolean', default: false, help: 'Unsave instead of save' },
|
|
89
|
+
],
|
|
90
|
+
func: vi.fn(),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
mockExecuteCommand.mockReset();
|
|
95
|
+
mockExecuteCommand.mockResolvedValue([]);
|
|
96
|
+
mockRenderOutput.mockReset();
|
|
97
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
98
|
+
process.exitCode = undefined;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('coerces default false for boolean args to a real boolean', async () => {
|
|
102
|
+
const program = new Command();
|
|
103
|
+
const siteCmd = program.command('reddit');
|
|
104
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
105
|
+
|
|
106
|
+
await program.parseAsync(['node', 'opencli', 'reddit', 'save', 't3_abc123']);
|
|
107
|
+
|
|
108
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
109
|
+
const kwargs = mockExecuteCommand.mock.calls[0][1];
|
|
110
|
+
expect(kwargs['post-id']).toBe('t3_abc123');
|
|
111
|
+
expect(kwargs.undo).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('coerces explicit false for boolean args to a real boolean', async () => {
|
|
115
|
+
const program = new Command();
|
|
116
|
+
const siteCmd = program.command('reddit');
|
|
117
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
118
|
+
|
|
119
|
+
await program.parseAsync(['node', 'opencli', 'reddit', 'save', 't3_abc123', '--undo', 'false']);
|
|
120
|
+
|
|
121
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
122
|
+
const kwargs = mockExecuteCommand.mock.calls[0][1];
|
|
123
|
+
expect(kwargs.undo).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/commanderAdapter.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
34
34
|
|
|
35
35
|
export function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown {
|
|
36
|
-
if (argType !== 'bool') return value;
|
|
36
|
+
if (argType !== 'bool' && argType !== 'boolean') return value;
|
|
37
37
|
if (typeof value === 'boolean') return value;
|
|
38
38
|
if (value == null || value === '') return false;
|
|
39
39
|
|
package/src/external-clis.yaml
CHANGED
|
@@ -30,6 +30,23 @@
|
|
|
30
30
|
install:
|
|
31
31
|
default: "npm install -g @larksuite/cli"
|
|
32
32
|
|
|
33
|
+
- name: dws
|
|
34
|
+
binary: dws
|
|
35
|
+
description: "DingTalk Workspace CLI — messages, docs, calendar, contacts and more for humans and AI agents"
|
|
36
|
+
homepage: "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
|
|
37
|
+
tags: [dingtalk, collaboration, productivity, ai-agent]
|
|
38
|
+
install:
|
|
39
|
+
mac: "curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh"
|
|
40
|
+
linux: "curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh"
|
|
41
|
+
|
|
42
|
+
- name: wecom-cli
|
|
43
|
+
binary: wecom-cli
|
|
44
|
+
description: "WeCom/企业微信 CLI — contacts, todos, meetings, messages, calendar, docs and smart sheets for AI agents"
|
|
45
|
+
homepage: "https://github.com/WecomTeam/wecom-cli"
|
|
46
|
+
tags: [wecom, wechat-work, collaboration, productivity, ai-agent]
|
|
47
|
+
install:
|
|
48
|
+
default: "npm install -g @wecom/cli"
|
|
49
|
+
|
|
33
50
|
- name: vercel
|
|
34
51
|
binary: vercel
|
|
35
52
|
description: "Vercel CLI — deploy projects, manage domains, env vars, logs and serverless functions"
|
package/src/types.ts
CHANGED
|
@@ -67,6 +67,11 @@ export interface IPage {
|
|
|
67
67
|
getInterceptedRequests(): Promise<any[]>;
|
|
68
68
|
waitForCapture(timeout?: number): Promise<void>;
|
|
69
69
|
screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
72
|
+
* Chrome reads the files directly — no base64 encoding or payload size limits.
|
|
73
|
+
*/
|
|
74
|
+
setFileInput?(files: string[], selector?: string): Promise<void>;
|
|
70
75
|
closeWindow?(): Promise<void>;
|
|
71
76
|
/** Returns the current page URL, or null if unavailable. */
|
|
72
77
|
getCurrentUrl?(): Promise<string | null>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
import { expectGracefulAuthFailure } from './browser-auth-helpers.js';
|
|
3
|
+
|
|
4
|
+
describe('band auth-required commands — graceful failure', () => {
|
|
5
|
+
it('band bands fails gracefully without login', async () => {
|
|
6
|
+
await expectGracefulAuthFailure(['band', 'bands', '-f', 'json']);
|
|
7
|
+
}, 60_000);
|
|
8
|
+
|
|
9
|
+
it('band mentions fails gracefully without login', async () => {
|
|
10
|
+
await expectGracefulAuthFailure(['band', 'mentions', '--limit', '3', '-f', 'json']);
|
|
11
|
+
}, 60_000);
|
|
12
|
+
|
|
13
|
+
it('band posts fails gracefully without login', async () => {
|
|
14
|
+
await expectGracefulAuthFailure(['band', 'posts', '58400480', '--limit', '3', '-f', 'json']);
|
|
15
|
+
}, 60_000);
|
|
16
|
+
|
|
17
|
+
it('band post fails gracefully without login', async () => {
|
|
18
|
+
await expectGracefulAuthFailure(['band', 'post', '58400480', '1', '-f', 'json']);
|
|
19
|
+
}, 60_000);
|
|
20
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
import { runCli } from './helpers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verify a login-required command fails gracefully (no crash, no hang).
|
|
6
|
+
* Acceptable outcomes: exit code 1 with error message, OR timeout handled.
|
|
7
|
+
*/
|
|
8
|
+
export async function expectGracefulAuthFailure(args: string[]) {
|
|
9
|
+
const { stdout, stderr, code } = await runCli(args, { timeout: 60_000 });
|
|
10
|
+
// Should either fail with exit code 1 (error message) or succeed with empty data.
|
|
11
|
+
// The key assertion: it should NOT hang forever or crash with unhandled exception.
|
|
12
|
+
if (code !== 0) {
|
|
13
|
+
// Verify stderr has a meaningful error, not an unhandled crash.
|
|
14
|
+
const output = stderr + stdout;
|
|
15
|
+
expect(output.length).toBeGreaterThan(0);
|
|
16
|
+
}
|
|
17
|
+
// If it somehow succeeds (e.g., partial public data), that's fine too.
|
|
18
|
+
}
|
|
@@ -6,148 +6,136 @@
|
|
|
6
6
|
* These tests verify the error handling path, not the data extraction.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, it
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Verify a login-required command fails gracefully (no crash, no hang).
|
|
14
|
-
* Acceptable outcomes: exit code 1 with error message, OR timeout handled.
|
|
15
|
-
*/
|
|
16
|
-
async function expectGracefulAuthFailure(args: string[], label: string) {
|
|
17
|
-
const { stdout, stderr, code } = await runCli(args, { timeout: 60_000 });
|
|
18
|
-
// Should either fail with exit code 1 (error message) or succeed with empty data
|
|
19
|
-
// The key assertion: it should NOT hang forever or crash with unhandled exception
|
|
20
|
-
if (code !== 0) {
|
|
21
|
-
// Verify stderr has a meaningful error, not an unhandled crash
|
|
22
|
-
const output = stderr + stdout;
|
|
23
|
-
expect(output.length).toBeGreaterThan(0);
|
|
24
|
-
}
|
|
25
|
-
// If it somehow succeeds (e.g., partial public data), that's fine too
|
|
26
|
-
}
|
|
9
|
+
import { describe, it } from 'vitest';
|
|
10
|
+
import { expectGracefulAuthFailure } from './browser-auth-helpers.js';
|
|
27
11
|
|
|
28
12
|
describe('login-required commands — graceful failure', () => {
|
|
29
13
|
|
|
30
14
|
// ── bilibili (requires cookie session) ──
|
|
31
15
|
it('bilibili me fails gracefully without login', async () => {
|
|
32
|
-
await expectGracefulAuthFailure(['bilibili', 'me', '-f', 'json']
|
|
16
|
+
await expectGracefulAuthFailure(['bilibili', 'me', '-f', 'json']);
|
|
33
17
|
}, 60_000);
|
|
34
18
|
|
|
35
19
|
it('bilibili dynamic fails gracefully without login', async () => {
|
|
36
|
-
await expectGracefulAuthFailure(['bilibili', 'dynamic', '--limit', '3', '-f', 'json']
|
|
20
|
+
await expectGracefulAuthFailure(['bilibili', 'dynamic', '--limit', '3', '-f', 'json']);
|
|
37
21
|
}, 60_000);
|
|
38
22
|
|
|
39
23
|
it('bilibili favorite fails gracefully without login', async () => {
|
|
40
|
-
await expectGracefulAuthFailure(['bilibili', 'favorite', '--limit', '3', '-f', 'json']
|
|
24
|
+
await expectGracefulAuthFailure(['bilibili', 'favorite', '--limit', '3', '-f', 'json']);
|
|
41
25
|
}, 60_000);
|
|
42
26
|
|
|
43
27
|
it('bilibili history fails gracefully without login', async () => {
|
|
44
|
-
await expectGracefulAuthFailure(['bilibili', 'history', '--limit', '3', '-f', 'json']
|
|
28
|
+
await expectGracefulAuthFailure(['bilibili', 'history', '--limit', '3', '-f', 'json']);
|
|
45
29
|
}, 60_000);
|
|
46
30
|
|
|
47
31
|
it('bilibili following fails gracefully without login', async () => {
|
|
48
|
-
await expectGracefulAuthFailure(['bilibili', 'following', '--limit', '3', '-f', 'json']
|
|
32
|
+
await expectGracefulAuthFailure(['bilibili', 'following', '--limit', '3', '-f', 'json']);
|
|
49
33
|
}, 60_000);
|
|
50
34
|
|
|
51
35
|
// ── twitter (requires login) ──
|
|
52
36
|
it('twitter bookmarks fails gracefully without login', async () => {
|
|
53
|
-
await expectGracefulAuthFailure(['twitter', 'bookmarks', '--limit', '3', '-f', 'json']
|
|
37
|
+
await expectGracefulAuthFailure(['twitter', 'bookmarks', '--limit', '3', '-f', 'json']);
|
|
54
38
|
}, 60_000);
|
|
55
39
|
|
|
56
40
|
it('twitter timeline fails gracefully without login', async () => {
|
|
57
|
-
await expectGracefulAuthFailure(['twitter', 'timeline', '--limit', '3', '-f', 'json']
|
|
41
|
+
await expectGracefulAuthFailure(['twitter', 'timeline', '--limit', '3', '-f', 'json']);
|
|
58
42
|
}, 60_000);
|
|
59
43
|
|
|
60
44
|
it('twitter notifications fails gracefully without login', async () => {
|
|
61
|
-
await expectGracefulAuthFailure(['twitter', 'notifications', '--limit', '3', '-f', 'json']
|
|
45
|
+
await expectGracefulAuthFailure(['twitter', 'notifications', '--limit', '3', '-f', 'json']);
|
|
62
46
|
}, 60_000);
|
|
63
47
|
|
|
64
48
|
// ── v2ex (requires login) ──
|
|
65
49
|
it('v2ex me fails gracefully without login', async () => {
|
|
66
|
-
await expectGracefulAuthFailure(['v2ex', 'me', '-f', 'json']
|
|
50
|
+
await expectGracefulAuthFailure(['v2ex', 'me', '-f', 'json']);
|
|
67
51
|
}, 60_000);
|
|
68
52
|
|
|
69
53
|
it('v2ex notifications fails gracefully without login', async () => {
|
|
70
|
-
await expectGracefulAuthFailure(['v2ex', 'notifications', '--limit', '3', '-f', 'json']
|
|
54
|
+
await expectGracefulAuthFailure(['v2ex', 'notifications', '--limit', '3', '-f', 'json']);
|
|
71
55
|
}, 60_000);
|
|
72
56
|
|
|
73
57
|
// ── xueqiu (requires login) ──
|
|
74
58
|
it('xueqiu feed fails gracefully without login', async () => {
|
|
75
|
-
await expectGracefulAuthFailure(['xueqiu', 'feed', '--limit', '3', '-f', 'json']
|
|
59
|
+
await expectGracefulAuthFailure(['xueqiu', 'feed', '--limit', '3', '-f', 'json']);
|
|
76
60
|
}, 60_000);
|
|
77
61
|
|
|
78
62
|
it('xueqiu watchlist fails gracefully without login', async () => {
|
|
79
|
-
await expectGracefulAuthFailure(['xueqiu', 'watchlist', '-f', 'json']
|
|
63
|
+
await expectGracefulAuthFailure(['xueqiu', 'watchlist', '-f', 'json']);
|
|
64
|
+
}, 60_000);
|
|
65
|
+
|
|
66
|
+
it('xueqiu comments fails gracefully without login', async () => {
|
|
67
|
+
await expectGracefulAuthFailure(['xueqiu', 'comments', 'SH600519', '--limit', '3', '-f', 'json'], 'xueqiu comments');
|
|
80
68
|
}, 60_000);
|
|
81
69
|
|
|
82
70
|
// ── linux-do (requires login — all endpoints need authentication) ──
|
|
83
71
|
it('linux-do feed fails gracefully without login', async () => {
|
|
84
|
-
await expectGracefulAuthFailure(['linux-do', 'feed', '--limit', '3', '-f', 'json']
|
|
72
|
+
await expectGracefulAuthFailure(['linux-do', 'feed', '--limit', '3', '-f', 'json']);
|
|
85
73
|
}, 60_000);
|
|
86
74
|
|
|
87
75
|
it('linux-do categories fails gracefully without login', async () => {
|
|
88
|
-
await expectGracefulAuthFailure(['linux-do', 'categories', '--limit', '3', '-f', 'json']
|
|
76
|
+
await expectGracefulAuthFailure(['linux-do', 'categories', '--limit', '3', '-f', 'json']);
|
|
89
77
|
}, 60_000);
|
|
90
78
|
|
|
91
79
|
it('linux-do tags fails gracefully without login', async () => {
|
|
92
|
-
await expectGracefulAuthFailure(['linux-do', 'tags', '--limit', '3', '-f', 'json']
|
|
80
|
+
await expectGracefulAuthFailure(['linux-do', 'tags', '--limit', '3', '-f', 'json']);
|
|
93
81
|
}, 60_000);
|
|
94
82
|
|
|
95
83
|
it('linux-do topic fails gracefully without login', async () => {
|
|
96
|
-
await expectGracefulAuthFailure(['linux-do', 'topic', '1', '-f', 'json']
|
|
84
|
+
await expectGracefulAuthFailure(['linux-do', 'topic', '1', '-f', 'json']);
|
|
97
85
|
}, 60_000);
|
|
98
86
|
|
|
99
87
|
it('linux-do search fails gracefully without login', async () => {
|
|
100
|
-
await expectGracefulAuthFailure(['linux-do', 'search', 'test', '--limit', '3', '-f', 'json']
|
|
88
|
+
await expectGracefulAuthFailure(['linux-do', 'search', 'test', '--limit', '3', '-f', 'json']);
|
|
101
89
|
}, 60_000);
|
|
102
90
|
|
|
103
91
|
it('linux-do user-topics fails gracefully without login', async () => {
|
|
104
|
-
await expectGracefulAuthFailure(['linux-do', 'user-topics', 'test', '--limit', '3', '-f', 'json']
|
|
92
|
+
await expectGracefulAuthFailure(['linux-do', 'user-topics', 'test', '--limit', '3', '-f', 'json']);
|
|
105
93
|
}, 60_000);
|
|
106
94
|
|
|
107
95
|
it('linux-do user-posts fails gracefully without login', async () => {
|
|
108
|
-
await expectGracefulAuthFailure(['linux-do', 'user-posts', 'test', '--limit', '3', '-f', 'json']
|
|
96
|
+
await expectGracefulAuthFailure(['linux-do', 'user-posts', 'test', '--limit', '3', '-f', 'json']);
|
|
109
97
|
}, 60_000);
|
|
110
98
|
|
|
111
99
|
// ── xiaohongshu (requires login) ──
|
|
112
100
|
it('xiaohongshu feed fails gracefully without login', async () => {
|
|
113
|
-
await expectGracefulAuthFailure(['xiaohongshu', 'feed', '--limit', '3', '-f', 'json']
|
|
101
|
+
await expectGracefulAuthFailure(['xiaohongshu', 'feed', '--limit', '3', '-f', 'json']);
|
|
114
102
|
}, 60_000);
|
|
115
103
|
|
|
116
104
|
it('xiaohongshu notifications fails gracefully without login', async () => {
|
|
117
|
-
await expectGracefulAuthFailure(['xiaohongshu', 'notifications', '--limit', '3', '-f', 'json']
|
|
105
|
+
await expectGracefulAuthFailure(['xiaohongshu', 'notifications', '--limit', '3', '-f', 'json']);
|
|
118
106
|
}, 60_000);
|
|
119
107
|
|
|
120
108
|
// ── pixiv (requires login) ──
|
|
121
109
|
it('pixiv ranking fails gracefully without login', async () => {
|
|
122
|
-
await expectGracefulAuthFailure(['pixiv', 'ranking', '--limit', '3', '-f', 'json']
|
|
110
|
+
await expectGracefulAuthFailure(['pixiv', 'ranking', '--limit', '3', '-f', 'json']);
|
|
123
111
|
}, 60_000);
|
|
124
112
|
|
|
125
113
|
it('pixiv search fails gracefully without login', async () => {
|
|
126
|
-
await expectGracefulAuthFailure(['pixiv', 'search', '初音ミク', '--limit', '3', '-f', 'json']
|
|
114
|
+
await expectGracefulAuthFailure(['pixiv', 'search', '初音ミク', '--limit', '3', '-f', 'json']);
|
|
127
115
|
}, 60_000);
|
|
128
116
|
|
|
129
117
|
it('pixiv user fails gracefully without login', async () => {
|
|
130
|
-
await expectGracefulAuthFailure(['pixiv', 'user', '11', '-f', 'json']
|
|
118
|
+
await expectGracefulAuthFailure(['pixiv', 'user', '11', '-f', 'json']);
|
|
131
119
|
}, 60_000);
|
|
132
120
|
|
|
133
121
|
it('pixiv illusts fails gracefully without login', async () => {
|
|
134
|
-
await expectGracefulAuthFailure(['pixiv', 'illusts', '11', '--limit', '3', '-f', 'json']
|
|
122
|
+
await expectGracefulAuthFailure(['pixiv', 'illusts', '11', '--limit', '3', '-f', 'json']);
|
|
135
123
|
}, 60_000);
|
|
136
124
|
|
|
137
125
|
it('pixiv detail fails gracefully without login', async () => {
|
|
138
|
-
await expectGracefulAuthFailure(['pixiv', 'detail', '123456', '-f', 'json']
|
|
126
|
+
await expectGracefulAuthFailure(['pixiv', 'detail', '123456', '-f', 'json']);
|
|
139
127
|
}, 60_000);
|
|
140
128
|
|
|
141
129
|
it('pixiv download fails gracefully without login', async () => {
|
|
142
|
-
await expectGracefulAuthFailure(['pixiv', 'download', '123456', '--output', '/tmp/pixiv-e2e-test', '-f', 'json']
|
|
130
|
+
await expectGracefulAuthFailure(['pixiv', 'download', '123456', '--output', '/tmp/pixiv-e2e-test', '-f', 'json']);
|
|
143
131
|
}, 60_000);
|
|
144
132
|
|
|
145
133
|
// ── yollomi (requires login session) ──
|
|
146
134
|
it('yollomi generate fails gracefully without login', async () => {
|
|
147
|
-
await expectGracefulAuthFailure(['yollomi', 'generate', 'a cute cat', '--no-download', '-f', 'json']
|
|
135
|
+
await expectGracefulAuthFailure(['yollomi', 'generate', 'a cute cat', '--no-download', '-f', 'json']);
|
|
148
136
|
}, 60_000);
|
|
149
137
|
|
|
150
138
|
it('yollomi video fails gracefully without login', async () => {
|
|
151
|
-
await expectGracefulAuthFailure(['yollomi', 'video', 'a sunset over the ocean', '--no-download', '-f', 'json']
|
|
139
|
+
await expectGracefulAuthFailure(['yollomi', 'video', 'a sunset over the ocean', '--no-download', '-f', 'json']);
|
|
152
140
|
}, 60_000);
|
|
153
141
|
});
|