@jackwener/opencli 1.1.0 → 1.1.1
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 +2 -2
- package/.github/pull_request_template.md +7 -0
- package/.github/workflows/doc-check.yml +36 -0
- package/.github/workflows/docs.yml +7 -42
- package/CHANGELOG.md +23 -0
- package/CLI-EXPLORER.md +9 -8
- package/README.md +25 -10
- package/README.zh-CN.md +26 -11
- package/SKILL.md +95 -31
- package/dist/browser/cdp.js +6 -1
- package/dist/browser/page.d.ts +4 -1
- package/dist/browser/page.js +7 -1
- package/dist/build-manifest.js +23 -16
- package/dist/cli-manifest.json +431 -276
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +189 -162
- package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
- package/dist/clis/apple-podcasts/commands.test.js +76 -0
- package/dist/clis/apple-podcasts/search.js +2 -2
- package/dist/clis/apple-podcasts/top.js +9 -2
- package/dist/clis/arxiv/search.js +1 -1
- package/dist/clis/bilibili/dynamic.js +1 -1
- package/dist/clis/bilibili/favorite.js +1 -1
- package/dist/clis/bilibili/feed.js +1 -1
- package/dist/clis/bilibili/following.js +1 -1
- package/dist/clis/bilibili/history.js +1 -1
- package/dist/clis/bilibili/me.js +1 -1
- package/dist/clis/bilibili/ranking.js +1 -1
- package/dist/clis/bilibili/search.js +3 -3
- package/dist/clis/bilibili/subtitle.js +1 -1
- package/dist/clis/bilibili/user-videos.js +1 -1
- package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
- package/dist/clis/bloomberg/businessweek.js +17 -0
- package/dist/clis/bloomberg/economics.js +17 -0
- package/dist/clis/bloomberg/feeds.d.ts +1 -0
- package/dist/clis/bloomberg/feeds.js +15 -0
- package/dist/clis/bloomberg/industries.d.ts +1 -0
- package/dist/clis/bloomberg/industries.js +17 -0
- package/dist/clis/bloomberg/main.d.ts +1 -0
- package/dist/clis/bloomberg/main.js +17 -0
- package/dist/clis/bloomberg/markets.d.ts +1 -0
- package/dist/clis/bloomberg/markets.js +17 -0
- package/dist/clis/bloomberg/news.d.ts +1 -0
- package/dist/clis/bloomberg/news.js +105 -0
- package/dist/clis/bloomberg/opinions.d.ts +1 -0
- package/dist/clis/bloomberg/opinions.js +17 -0
- package/dist/clis/bloomberg/politics.d.ts +1 -0
- package/dist/clis/bloomberg/politics.js +17 -0
- package/dist/clis/bloomberg/tech.d.ts +1 -0
- package/dist/clis/bloomberg/tech.js +17 -0
- package/dist/clis/bloomberg/utils.d.ts +34 -0
- package/dist/clis/bloomberg/utils.js +364 -0
- package/dist/clis/bloomberg/utils.test.d.ts +1 -0
- package/dist/clis/bloomberg/utils.test.js +129 -0
- package/dist/clis/boss/batchgreet.js +2 -2
- package/dist/clis/boss/chatlist.js +2 -2
- package/dist/clis/boss/detail.js +2 -2
- package/dist/clis/boss/greet.js +4 -4
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +1 -1
- package/dist/clis/boss/stats.js +2 -2
- package/dist/clis/chaoxing/assignments.js +1 -1
- package/dist/clis/chaoxing/exams.js +1 -1
- package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
- package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
- package/dist/clis/chaoxing/utils.test.d.ts +1 -0
- package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
- package/dist/clis/chatgpt/read.js +1 -1
- package/dist/clis/chatwise/export.js +1 -1
- package/dist/clis/chatwise/model.js +2 -2
- package/dist/clis/chatwise/screenshot.js +1 -1
- package/dist/clis/codex/export.js +1 -1
- package/dist/clis/codex/model.js +2 -2
- package/dist/clis/codex/screenshot.js +1 -1
- package/dist/clis/coupang/add-to-cart.js +3 -4
- package/dist/clis/coupang/search.js +2 -4
- package/dist/clis/coupang/utils.test.d.ts +1 -0
- package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
- package/dist/clis/ctrip/search.js +1 -1
- package/dist/clis/cursor/export.js +1 -1
- package/dist/clis/cursor/model.js +2 -2
- package/dist/clis/cursor/screenshot.js +1 -1
- package/dist/clis/jike/comment.js +2 -3
- package/dist/clis/jike/create.js +1 -2
- package/dist/clis/jike/feed.js +0 -1
- package/dist/clis/jike/like.js +1 -2
- package/dist/clis/jike/notifications.js +0 -1
- package/dist/clis/jike/post.yaml +1 -0
- package/dist/clis/jike/repost.js +1 -2
- package/dist/clis/jike/search.js +2 -3
- package/dist/clis/jike/topic.yaml +1 -0
- package/dist/clis/jike/user.yaml +1 -0
- package/dist/clis/jimeng/history.yaml +0 -1
- package/dist/clis/linkedin/search.js +7 -7
- package/dist/clis/linux-do/category.yaml +1 -0
- package/dist/clis/linux-do/search.yaml +4 -3
- package/dist/clis/linux-do/topic.yaml +1 -0
- package/dist/clis/notion/export.js +1 -1
- package/dist/clis/reddit/comment.js +3 -4
- package/dist/clis/reddit/read.js +4 -5
- package/dist/clis/reddit/save.js +2 -3
- package/dist/clis/reddit/saved.js +0 -1
- package/dist/clis/reddit/search.yaml +1 -0
- package/dist/clis/reddit/subscribe.js +0 -1
- package/dist/clis/reddit/upvote.js +2 -3
- package/dist/clis/reddit/upvoted.js +0 -1
- package/dist/clis/reddit/user-comments.yaml +1 -0
- package/dist/clis/reddit/user-posts.yaml +1 -0
- package/dist/clis/reddit/user.yaml +1 -0
- package/dist/clis/reuters/search.js +1 -1
- package/dist/clis/smzdm/search.js +2 -3
- package/dist/clis/stackoverflow/search.yaml +1 -0
- package/dist/clis/steam/top-sellers.yaml +29 -0
- package/dist/clis/twitter/accept.js +2 -2
- package/dist/clis/twitter/article.js +2 -2
- package/dist/clis/twitter/block.d.ts +1 -0
- package/dist/clis/twitter/block.js +88 -0
- package/dist/clis/twitter/delete.js +1 -1
- package/dist/clis/twitter/hide-reply.d.ts +1 -0
- package/dist/clis/twitter/hide-reply.js +66 -0
- package/dist/clis/twitter/like.js +1 -1
- package/dist/clis/twitter/post.js +1 -1
- package/dist/clis/twitter/reply-dm.js +1 -1
- package/dist/clis/twitter/reply.js +2 -2
- package/dist/clis/twitter/search.js +1 -1
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/trending.d.ts +1 -0
- package/dist/clis/twitter/trending.js +91 -0
- package/dist/clis/twitter/unblock.d.ts +1 -0
- package/dist/clis/twitter/unblock.js +71 -0
- package/dist/clis/v2ex/topic.yaml +1 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weread/book.js +1 -1
- package/dist/clis/weread/highlights.js +1 -1
- package/dist/clis/weread/notes.js +1 -1
- package/dist/clis/weread/search.js +1 -1
- package/dist/clis/wikipedia/search.js +1 -1
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +15 -0
- package/dist/clis/xiaohongshu/creator-note-detail.js +69 -5
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +80 -33
- package/dist/clis/xiaohongshu/creator-notes.js +35 -5
- package/dist/clis/xiaohongshu/creator-notes.test.js +35 -6
- package/dist/clis/xiaohongshu/creator-profile.js +0 -1
- package/dist/clis/xiaohongshu/creator-stats.js +0 -1
- package/dist/clis/xiaohongshu/download.js +2 -3
- package/dist/clis/xiaohongshu/feed.yaml +0 -1
- package/dist/clis/xiaohongshu/notifications.yaml +0 -1
- package/dist/clis/xiaohongshu/search.js +2 -2
- package/dist/clis/xiaohongshu/user.js +1 -2
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/search.js +1 -1
- package/dist/clis/youtube/transcript.js +1 -1
- package/dist/clis/youtube/video.js +1 -1
- package/dist/clis/zhihu/download.js +1 -2
- package/dist/clis/zhihu/question.js +1 -1
- package/dist/clis/zhihu/search.yaml +4 -3
- package/dist/commanderAdapter.d.ts +21 -0
- package/dist/commanderAdapter.js +111 -0
- package/dist/{engine.d.ts → discovery.d.ts} +0 -6
- package/dist/{engine.js → discovery.js} +1 -98
- package/dist/download/index.d.ts +2 -6
- package/dist/download/index.js +19 -46
- package/dist/engine.test.d.ts +1 -1
- package/dist/engine.test.js +8 -7
- package/dist/execution.d.ts +22 -0
- package/dist/execution.js +129 -0
- package/dist/explore.js +121 -107
- package/dist/external-clis.yaml +48 -0
- package/dist/external.d.ts +7 -2
- package/dist/external.js +11 -14
- package/dist/main.js +1 -1
- package/dist/pipeline/steps/browser.js +8 -2
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +2 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +8 -0
- package/dist/serialization.d.ts +34 -0
- package/dist/serialization.js +63 -0
- package/dist/types.d.ts +4 -1
- package/docs/.vitepress/config.mts +14 -3
- package/docs/adapters/browser/arxiv.md +27 -0
- package/docs/adapters/browser/barchart.md +32 -0
- package/docs/adapters/browser/bloomberg.md +70 -0
- package/docs/adapters/browser/chaoxing.md +39 -0
- package/docs/adapters/browser/grok.md +35 -0
- package/docs/adapters/browser/hf.md +42 -0
- package/docs/adapters/browser/jike.md +45 -0
- package/docs/adapters/browser/jimeng.md +39 -0
- package/docs/adapters/browser/linux-do.md +45 -0
- package/docs/adapters/browser/sinafinance.md +35 -0
- package/docs/adapters/browser/stackoverflow.md +35 -0
- package/docs/adapters/browser/steam.md +26 -0
- package/docs/adapters/browser/twitter.md +3 -0
- package/docs/adapters/browser/weread.md +48 -0
- package/docs/adapters/browser/wikipedia.md +30 -0
- package/docs/adapters/browser/xiaohongshu.md +5 -1
- package/docs/adapters/desktop/chatgpt.md +3 -3
- package/docs/adapters/index.md +13 -0
- package/docs/advanced/download.md +4 -4
- package/docs/developer/architecture.md +17 -4
- package/package.json +1 -1
- package/scripts/check-doc-coverage.sh +69 -0
- package/scripts/copy-yaml.cjs +7 -0
- package/src/browser/cdp.ts +6 -1
- package/src/browser/page.ts +7 -1
- package/src/build-manifest.ts +25 -19
- package/src/cli.ts +218 -139
- package/src/clis/apple-podcasts/commands.test.ts +95 -0
- package/src/clis/apple-podcasts/search.ts +2 -2
- package/src/clis/apple-podcasts/top.ts +12 -2
- package/src/clis/arxiv/search.ts +1 -1
- package/src/clis/bilibili/dynamic.ts +1 -1
- package/src/clis/bilibili/favorite.ts +1 -1
- package/src/clis/bilibili/feed.ts +1 -1
- package/src/clis/bilibili/following.ts +1 -1
- package/src/clis/bilibili/history.ts +1 -1
- package/src/clis/bilibili/me.ts +1 -1
- package/src/clis/bilibili/ranking.ts +1 -1
- package/src/clis/bilibili/search.ts +3 -3
- package/src/clis/bilibili/subtitle.ts +1 -1
- package/src/clis/bilibili/user-videos.ts +1 -1
- package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
- package/src/clis/bloomberg/businessweek.ts +18 -0
- package/src/clis/bloomberg/economics.ts +18 -0
- package/src/clis/bloomberg/feeds.ts +16 -0
- package/src/clis/bloomberg/industries.ts +18 -0
- package/src/clis/bloomberg/main.ts +18 -0
- package/src/clis/bloomberg/markets.ts +18 -0
- package/src/clis/bloomberg/news.ts +136 -0
- package/src/clis/bloomberg/opinions.ts +18 -0
- package/src/clis/bloomberg/politics.ts +18 -0
- package/src/clis/bloomberg/tech.ts +18 -0
- package/src/clis/bloomberg/utils.test.ts +135 -0
- package/src/clis/bloomberg/utils.ts +429 -0
- package/src/clis/boss/batchgreet.ts +2 -2
- package/src/clis/boss/chatlist.ts +2 -2
- package/src/clis/boss/detail.ts +2 -2
- package/src/clis/boss/greet.ts +4 -4
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +1 -1
- package/src/clis/boss/stats.ts +2 -2
- package/src/clis/chaoxing/assignments.ts +1 -1
- package/src/clis/chaoxing/exams.ts +1 -1
- package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
- package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
- package/src/clis/chatgpt/README.zh-CN.md +3 -3
- package/src/clis/chatgpt/read.ts +1 -1
- package/src/clis/chatwise/export.ts +1 -1
- package/src/clis/chatwise/model.ts +2 -2
- package/src/clis/chatwise/screenshot.ts +1 -1
- package/src/clis/codex/export.ts +1 -1
- package/src/clis/codex/model.ts +2 -2
- package/src/clis/codex/screenshot.ts +1 -1
- package/src/clis/coupang/add-to-cart.ts +3 -4
- package/src/clis/coupang/search.ts +2 -4
- package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
- package/src/clis/ctrip/search.ts +1 -1
- package/src/clis/cursor/export.ts +1 -1
- package/src/clis/cursor/model.ts +2 -2
- package/src/clis/cursor/screenshot.ts +1 -1
- package/src/clis/jike/comment.ts +2 -3
- package/src/clis/jike/create.ts +1 -2
- package/src/clis/jike/feed.ts +0 -1
- package/src/clis/jike/like.ts +1 -2
- package/src/clis/jike/notifications.ts +0 -1
- package/src/clis/jike/post.yaml +1 -0
- package/src/clis/jike/repost.ts +1 -2
- package/src/clis/jike/search.ts +2 -3
- package/src/clis/jike/topic.yaml +1 -0
- package/src/clis/jike/user.yaml +1 -0
- package/src/clis/jimeng/history.yaml +0 -1
- package/src/clis/linkedin/search.ts +7 -7
- package/src/clis/linux-do/category.yaml +1 -0
- package/src/clis/linux-do/search.yaml +4 -3
- package/src/clis/linux-do/topic.yaml +1 -0
- package/src/clis/notion/export.ts +1 -1
- package/src/clis/reddit/comment.ts +3 -4
- package/src/clis/reddit/read.ts +4 -5
- package/src/clis/reddit/save.ts +2 -3
- package/src/clis/reddit/saved.ts +0 -1
- package/src/clis/reddit/search.yaml +1 -0
- package/src/clis/reddit/subscribe.ts +0 -1
- package/src/clis/reddit/upvote.ts +2 -3
- package/src/clis/reddit/upvoted.ts +0 -1
- package/src/clis/reddit/user-comments.yaml +1 -0
- package/src/clis/reddit/user-posts.yaml +1 -0
- package/src/clis/reddit/user.yaml +1 -0
- package/src/clis/reuters/search.ts +1 -1
- package/src/clis/smzdm/search.ts +2 -3
- package/src/clis/stackoverflow/search.yaml +1 -0
- package/src/clis/steam/top-sellers.yaml +29 -0
- package/src/clis/twitter/accept.ts +2 -2
- package/src/clis/twitter/article.ts +2 -2
- package/src/clis/twitter/block.ts +92 -0
- package/src/clis/twitter/delete.ts +1 -1
- package/src/clis/twitter/hide-reply.ts +70 -0
- package/src/clis/twitter/like.ts +1 -1
- package/src/clis/twitter/post.ts +1 -1
- package/src/clis/twitter/reply-dm.ts +1 -1
- package/src/clis/twitter/reply.ts +2 -2
- package/src/clis/twitter/search.ts +1 -1
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/trending.ts +113 -0
- package/src/clis/twitter/unblock.ts +75 -0
- package/src/clis/v2ex/topic.yaml +1 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weread/book.ts +1 -1
- package/src/clis/weread/highlights.ts +1 -1
- package/src/clis/weread/notes.ts +1 -1
- package/src/clis/weread/search.ts +1 -1
- package/src/clis/wikipedia/search.ts +1 -1
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +82 -33
- package/src/clis/xiaohongshu/creator-note-detail.ts +89 -5
- package/src/clis/xiaohongshu/creator-notes.test.ts +39 -6
- package/src/clis/xiaohongshu/creator-notes.ts +44 -5
- package/src/clis/xiaohongshu/creator-profile.ts +0 -1
- package/src/clis/xiaohongshu/creator-stats.ts +0 -1
- package/src/clis/xiaohongshu/download.ts +2 -3
- package/src/clis/xiaohongshu/feed.yaml +0 -1
- package/src/clis/xiaohongshu/notifications.yaml +0 -1
- package/src/clis/xiaohongshu/search.ts +2 -2
- package/src/clis/xiaohongshu/user.ts +1 -2
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/search.ts +1 -1
- package/src/clis/youtube/transcript.ts +1 -1
- package/src/clis/youtube/video.ts +1 -1
- package/src/clis/zhihu/download.ts +1 -2
- package/src/clis/zhihu/question.ts +1 -1
- package/src/clis/zhihu/search.yaml +4 -3
- package/src/commanderAdapter.ts +113 -0
- package/src/{engine.ts → discovery.ts} +1 -108
- package/src/download/index.ts +21 -54
- package/src/engine.test.ts +8 -7
- package/src/execution.ts +138 -0
- package/src/explore.ts +135 -109
- package/src/external-clis.yaml +9 -0
- package/src/external.ts +15 -12
- package/src/main.ts +1 -1
- package/src/pipeline/steps/browser.ts +7 -2
- package/src/registry.ts +5 -0
- package/src/runtime.ts +9 -0
- package/src/serialization.ts +79 -0
- package/src/types.ts +1 -1
- package/tests/e2e/browser-public.test.ts +25 -0
- package/tests/e2e/public-commands.test.ts +55 -1
- package/dist/clis/twitter/trending.yaml +0 -46
- package/docs/public/CNAME +0 -1
- package/src/clis/twitter/trending.yaml +0 -46
- /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
- /package/dist/{chaoxing.test.d.ts → clis/bloomberg/businessweek.d.ts} +0 -0
- /package/dist/{coupang.test.d.ts → clis/bloomberg/economics.d.ts} +0 -0
- /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
- /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
- /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
|
@@ -21,6 +21,23 @@ type CreatorNoteDetailRow = {
|
|
|
21
21
|
|
|
22
22
|
export type { CreatorNoteDetailRow };
|
|
23
23
|
|
|
24
|
+
type CreatorNoteDetailDomMetric = {
|
|
25
|
+
label: string;
|
|
26
|
+
value: string;
|
|
27
|
+
extra: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type CreatorNoteDetailDomSection = {
|
|
31
|
+
title: string;
|
|
32
|
+
metrics: CreatorNoteDetailDomMetric[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type CreatorNoteDetailDomData = {
|
|
36
|
+
title: string;
|
|
37
|
+
infoText: string;
|
|
38
|
+
sections: CreatorNoteDetailDomSection[];
|
|
39
|
+
};
|
|
40
|
+
|
|
24
41
|
type AudienceSourceItem = {
|
|
25
42
|
title?: string;
|
|
26
43
|
value_with_double?: number;
|
|
@@ -87,6 +104,7 @@ const NOTE_DETAIL_METRICS = [
|
|
|
87
104
|
] as const;
|
|
88
105
|
|
|
89
106
|
const NOTE_DETAIL_METRIC_LABELS = new Set<string>(NOTE_DETAIL_METRICS.map((metric) => metric.label));
|
|
107
|
+
const NOTE_DETAIL_SECTIONS = new Set<string>(NOTE_DETAIL_METRICS.map((metric) => metric.section));
|
|
90
108
|
const NOTE_DETAIL_NOISE_LINES = new Set([
|
|
91
109
|
'切换笔记',
|
|
92
110
|
'笔记诊断',
|
|
@@ -144,6 +162,11 @@ function findMetricValue(lines: string[], startIndex: number): { value: string;
|
|
|
144
162
|
return { value, extra };
|
|
145
163
|
}
|
|
146
164
|
|
|
165
|
+
function findPublishedAt(text: string): string {
|
|
166
|
+
const match = text.match(/\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/);
|
|
167
|
+
return match?.[0] ?? '';
|
|
168
|
+
}
|
|
169
|
+
|
|
147
170
|
export function parseCreatorNoteDetailText(bodyText: string, noteId: string): CreatorNoteDetailRow[] {
|
|
148
171
|
const lines = bodyText
|
|
149
172
|
.split('\n')
|
|
@@ -173,6 +196,35 @@ export function parseCreatorNoteDetailText(bodyText: string, noteId: string): Cr
|
|
|
173
196
|
return rows;
|
|
174
197
|
}
|
|
175
198
|
|
|
199
|
+
export function parseCreatorNoteDetailDomData(dom: CreatorNoteDetailDomData | null | undefined, noteId: string): CreatorNoteDetailRow[] {
|
|
200
|
+
if (!dom) return [];
|
|
201
|
+
const title = typeof dom.title === 'string' ? dom.title.trim() : '';
|
|
202
|
+
const infoText = typeof dom.infoText === 'string' ? dom.infoText : '';
|
|
203
|
+
const sections = Array.isArray(dom.sections) ? dom.sections : [];
|
|
204
|
+
|
|
205
|
+
const rows: CreatorNoteDetailRow[] = [
|
|
206
|
+
{ section: '笔记信息', metric: 'note_id', value: noteId, extra: '' },
|
|
207
|
+
{ section: '笔记信息', metric: 'title', value: title, extra: '' },
|
|
208
|
+
{ section: '笔记信息', metric: 'published_at', value: findPublishedAt(infoText), extra: '' },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
for (const section of sections) {
|
|
212
|
+
if (!NOTE_DETAIL_SECTIONS.has(section.title)) continue;
|
|
213
|
+
for (const metric of section.metrics) {
|
|
214
|
+
if (!NOTE_DETAIL_METRIC_LABELS.has(metric.label)) continue;
|
|
215
|
+
rows.push({
|
|
216
|
+
section: section.title,
|
|
217
|
+
metric: metric.label,
|
|
218
|
+
value: metric.value,
|
|
219
|
+
extra: metric.extra,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const hasMetric = rows.some((row) => row.section !== '笔记信息' && row.value);
|
|
225
|
+
return hasMetric ? rows : [];
|
|
226
|
+
}
|
|
227
|
+
|
|
176
228
|
function toPercentString(value?: number): string {
|
|
177
229
|
return value == null ? '' : `${value}%`;
|
|
178
230
|
}
|
|
@@ -325,12 +377,44 @@ async function captureNoteDetailPayload(page: IPage, noteId: string): Promise<No
|
|
|
325
377
|
return captured > 0 ? payload : null;
|
|
326
378
|
}
|
|
327
379
|
|
|
380
|
+
async function captureNoteDetailDomData(page: IPage): Promise<CreatorNoteDetailDomData | null> {
|
|
381
|
+
const result = await page.evaluate(`() => {
|
|
382
|
+
const norm = (value) => (value || '').trim();
|
|
383
|
+
const sections = Array.from(document.querySelectorAll('.shell-container')).map((container) => {
|
|
384
|
+
const containerText = norm(container.innerText);
|
|
385
|
+
const title = containerText.startsWith('互动数据')
|
|
386
|
+
? '互动数据'
|
|
387
|
+
: containerText.includes('基础数据')
|
|
388
|
+
? '基础数据'
|
|
389
|
+
: '';
|
|
390
|
+
const metrics = Array.from(container.querySelectorAll('.block-container.block')).map((block) => ({
|
|
391
|
+
label: norm(block.querySelector('.des')?.innerText),
|
|
392
|
+
value: norm(block.querySelector('.content')?.innerText),
|
|
393
|
+
extra: norm(block.querySelector('.text-with-fans')?.innerText),
|
|
394
|
+
})).filter((metric) => metric.label && metric.value);
|
|
395
|
+
return { title, metrics };
|
|
396
|
+
}).filter((section) => section.title && section.metrics.length > 0);
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
title: norm(document.querySelector('.note-title')?.innerText),
|
|
400
|
+
infoText: norm(document.querySelector('.note-info-content')?.innerText),
|
|
401
|
+
sections,
|
|
402
|
+
};
|
|
403
|
+
}`);
|
|
404
|
+
|
|
405
|
+
if (!result || typeof result !== 'object') return null;
|
|
406
|
+
return result as CreatorNoteDetailDomData;
|
|
407
|
+
}
|
|
408
|
+
|
|
328
409
|
export async function fetchCreatorNoteDetailRows(page: IPage, noteId: string): Promise<CreatorNoteDetailRow[]> {
|
|
329
410
|
await page.goto(`https://creator.xiaohongshu.com/statistics/note-detail?noteId=${encodeURIComponent(noteId)}`);
|
|
330
|
-
await page.wait(4);
|
|
331
411
|
|
|
332
|
-
const
|
|
333
|
-
|
|
412
|
+
const domData = await captureNoteDetailDomData(page).catch(() => null);
|
|
413
|
+
let rows = parseCreatorNoteDetailDomData(domData, noteId);
|
|
414
|
+
if (rows.length === 0) {
|
|
415
|
+
const bodyText = await page.evaluate('() => document.body.innerText');
|
|
416
|
+
rows = parseCreatorNoteDetailText(typeof bodyText === 'string' ? bodyText : '', noteId);
|
|
417
|
+
}
|
|
334
418
|
const apiPayload = await captureNoteDetailPayload(page, noteId).catch(() => null);
|
|
335
419
|
appendTrendRows(rows, apiPayload ?? undefined);
|
|
336
420
|
appendAudienceRows(rows, apiPayload ?? undefined);
|
|
@@ -346,11 +430,11 @@ cli({
|
|
|
346
430
|
strategy: Strategy.COOKIE,
|
|
347
431
|
browser: true,
|
|
348
432
|
args: [
|
|
349
|
-
{ name: '
|
|
433
|
+
{ name: 'note-id', type: 'string', required: true, help: 'Note ID (from creator-notes or note-detail page URL)' },
|
|
350
434
|
],
|
|
351
435
|
columns: ['section', 'metric', 'value', 'extra'],
|
|
352
436
|
func: async (page, kwargs) => {
|
|
353
|
-
const noteId: string = kwargs
|
|
437
|
+
const noteId: string = kwargs['note-id'];
|
|
354
438
|
const rows = await fetchCreatorNoteDetailRows(page, noteId);
|
|
355
439
|
|
|
356
440
|
const hasCoreMetric = rows.some((row) => row.section !== '笔记信息' && row.value);
|
|
@@ -70,9 +70,9 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
70
70
|
title: '神雕侠侣战力金字塔',
|
|
71
71
|
date: '2025年12月04日 19:45',
|
|
72
72
|
views: 148208,
|
|
73
|
-
likes:
|
|
74
|
-
collects:
|
|
75
|
-
comments:
|
|
73
|
+
likes: 2279,
|
|
74
|
+
collects: 465,
|
|
75
|
+
comments: 324,
|
|
76
76
|
url: '',
|
|
77
77
|
},
|
|
78
78
|
{
|
|
@@ -117,14 +117,47 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
117
117
|
title: '示例笔记',
|
|
118
118
|
date: '2026年03月19日 12:00',
|
|
119
119
|
views: 10,
|
|
120
|
-
likes:
|
|
121
|
-
collects:
|
|
122
|
-
comments:
|
|
120
|
+
likes: 3,
|
|
121
|
+
collects: 4,
|
|
122
|
+
comments: 2,
|
|
123
123
|
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
|
|
124
124
|
},
|
|
125
125
|
]);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
it('prefers note card dom data when the analyze api is unavailable', async () => {
|
|
129
|
+
const cmd = getRegistry().get('xiaohongshu/creator-notes');
|
|
130
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
131
|
+
|
|
132
|
+
const page = createPageMock([
|
|
133
|
+
undefined,
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
id: '693155fc000000000d03b42c',
|
|
137
|
+
title: '神雕侠侣战力金字塔',
|
|
138
|
+
date: '2025年12月04日 19:45',
|
|
139
|
+
metrics: [148284, 319, 2280, 466, 33],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const result = await cmd!.func!(page, { limit: 1 });
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual([
|
|
147
|
+
{
|
|
148
|
+
rank: 1,
|
|
149
|
+
id: '693155fc000000000d03b42c',
|
|
150
|
+
title: '神雕侠侣战力金字塔',
|
|
151
|
+
date: '2025年12月04日 19:45',
|
|
152
|
+
views: 148284,
|
|
153
|
+
likes: 2280,
|
|
154
|
+
collects: 466,
|
|
155
|
+
comments: 319,
|
|
156
|
+
url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=693155fc000000000d03b42c',
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
|
|
128
161
|
it('prefers the creator analyze API and preserves note ids', async () => {
|
|
129
162
|
const cmd = getRegistry().get('xiaohongshu/creator-notes');
|
|
130
163
|
expect(cmd?.func).toBeTypeOf('function');
|
|
@@ -30,6 +30,13 @@ type CreatorNoteRow = {
|
|
|
30
30
|
|
|
31
31
|
export type { CreatorNoteRow };
|
|
32
32
|
|
|
33
|
+
type CreatorNoteDomCard = {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
date: string;
|
|
37
|
+
metrics: number[];
|
|
38
|
+
};
|
|
39
|
+
|
|
33
40
|
type CreatorAnalyzeApiResponse = {
|
|
34
41
|
error?: string;
|
|
35
42
|
data?: {
|
|
@@ -97,9 +104,9 @@ export function parseCreatorNotesText(bodyText: string): CreatorNoteRow[] {
|
|
|
97
104
|
title,
|
|
98
105
|
date: dateMatch[1],
|
|
99
106
|
views: metrics[0] ?? 0,
|
|
100
|
-
likes: metrics[
|
|
101
|
-
collects: metrics[
|
|
102
|
-
comments: metrics[
|
|
107
|
+
likes: metrics[2] ?? 0,
|
|
108
|
+
collects: metrics[3] ?? 0,
|
|
109
|
+
comments: metrics[1] ?? 0,
|
|
103
110
|
url: '',
|
|
104
111
|
});
|
|
105
112
|
|
|
@@ -123,6 +130,19 @@ export function parseCreatorNoteIdsFromHtml(bodyHtml: string): string[] {
|
|
|
123
130
|
return ids;
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
function mapDomCards(cards: CreatorNoteDomCard[]): CreatorNoteRow[] {
|
|
134
|
+
return cards.map((card) => ({
|
|
135
|
+
id: card.id,
|
|
136
|
+
title: card.title,
|
|
137
|
+
date: card.date,
|
|
138
|
+
views: card.metrics[0] ?? 0,
|
|
139
|
+
likes: card.metrics[2] ?? 0,
|
|
140
|
+
collects: card.metrics[3] ?? 0,
|
|
141
|
+
comments: card.metrics[1] ?? 0,
|
|
142
|
+
url: buildNoteDetailUrl(card.id),
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
function mapAnalyzeItems(items: NonNullable<CreatorAnalyzeApiResponse['data']>['note_infos']): CreatorNoteRow[] {
|
|
127
147
|
return (items ?? []).map((item) => ({
|
|
128
148
|
id: item.id ?? '',
|
|
@@ -142,7 +162,6 @@ async function fetchCreatorNotesByApi(page: IPage, limit: number): Promise<Creat
|
|
|
142
162
|
const notes: CreatorNoteRow[] = [];
|
|
143
163
|
|
|
144
164
|
await page.goto(`https://creator.xiaohongshu.com/statistics/data-analysis?type=0&page_size=${pageSize}&page_num=1`);
|
|
145
|
-
await page.wait(4);
|
|
146
165
|
|
|
147
166
|
for (let pageNum = 1; pageNum <= maxPages && notes.length < limit; pageNum++) {
|
|
148
167
|
const apiPath = `${NOTE_ANALYZE_API_PATH}?type=0&page_size=${pageSize}&page_num=${pageNum}`;
|
|
@@ -190,10 +209,30 @@ export async function fetchCreatorNotes(page: IPage, limit: number): Promise<Cre
|
|
|
190
209
|
|
|
191
210
|
if (notes.length === 0) {
|
|
192
211
|
await page.goto('https://creator.xiaohongshu.com/new/note-manager');
|
|
193
|
-
await page.wait(4);
|
|
194
212
|
|
|
195
213
|
const maxPageDowns = Math.max(0, Math.ceil(limit / 10) + 1);
|
|
196
214
|
for (let i = 0; i <= maxPageDowns; i++) {
|
|
215
|
+
const domCards = await page.evaluate(`() => {
|
|
216
|
+
const noteIdRe = /"noteId":"([0-9a-f]{24})"/;
|
|
217
|
+
return Array.from(document.querySelectorAll('div.note[data-impression], div.note')).map((card) => {
|
|
218
|
+
const impression = card.getAttribute('data-impression') || '';
|
|
219
|
+
const id = impression.match(noteIdRe)?.[1] || '';
|
|
220
|
+
const title = (card.querySelector('.title, .raw')?.innerText || '').trim();
|
|
221
|
+
const dateText = (card.querySelector('.time_status, .time')?.innerText || '').trim();
|
|
222
|
+
const date = dateText.replace(/^发布于\\s*/, '');
|
|
223
|
+
const metrics = Array.from(card.querySelectorAll('.icon_list .icon'))
|
|
224
|
+
.map((el) => parseInt((el.innerText || '').trim(), 10))
|
|
225
|
+
.filter((value) => Number.isFinite(value));
|
|
226
|
+
return { id, title, date, metrics };
|
|
227
|
+
});
|
|
228
|
+
}`) as CreatorNoteDomCard[] | undefined;
|
|
229
|
+
const parsedDomNotes = mapDomCards(Array.isArray(domCards) ? domCards : []).filter((note) => note.title && note.date);
|
|
230
|
+
if (parsedDomNotes.length > 0) {
|
|
231
|
+
notes = parsedDomNotes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (notes.length >= limit || (notes.length > 0 && i === 0)) break;
|
|
235
|
+
|
|
197
236
|
const body = await page.evaluate('() => ({ text: document.body.innerText, html: document.body.innerHTML })') as {
|
|
198
237
|
text?: string;
|
|
199
238
|
html?: string;
|
|
@@ -22,17 +22,16 @@ cli({
|
|
|
22
22
|
domain: 'www.xiaohongshu.com',
|
|
23
23
|
strategy: Strategy.COOKIE,
|
|
24
24
|
args: [
|
|
25
|
-
{ name: '
|
|
25
|
+
{ name: 'note-id', required: true, help: 'Note ID (from URL)' },
|
|
26
26
|
{ name: 'output', default: './xiaohongshu-downloads', help: 'Output directory' },
|
|
27
27
|
],
|
|
28
28
|
columns: ['index', 'type', 'status', 'size'],
|
|
29
29
|
func: async (page, kwargs) => {
|
|
30
|
-
const noteId = kwargs
|
|
30
|
+
const noteId = kwargs['note-id'];
|
|
31
31
|
const output = kwargs.output;
|
|
32
32
|
|
|
33
33
|
// Navigate to note page
|
|
34
34
|
await page.goto(`https://www.xiaohongshu.com/explore/${noteId}`);
|
|
35
|
-
await page.wait(3);
|
|
36
35
|
|
|
37
36
|
// Extract note info and media URLs
|
|
38
37
|
const data = await page.evaluate(`
|
|
@@ -15,12 +15,12 @@ cli({
|
|
|
15
15
|
domain: 'www.xiaohongshu.com',
|
|
16
16
|
strategy: Strategy.COOKIE,
|
|
17
17
|
args: [
|
|
18
|
-
{ name: '
|
|
18
|
+
{ name: 'query', required: true, positional: true, help: 'Search keyword' },
|
|
19
19
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
20
20
|
],
|
|
21
21
|
columns: ['rank', 'title', 'author', 'likes'],
|
|
22
22
|
func: async (page, kwargs) => {
|
|
23
|
-
const keyword = encodeURIComponent(kwargs.
|
|
23
|
+
const keyword = encodeURIComponent(kwargs.query);
|
|
24
24
|
await page.goto(
|
|
25
25
|
`https://www.xiaohongshu.com/search_result?keyword=${keyword}&source=web_search_result_notes`
|
|
26
26
|
);
|
|
@@ -29,7 +29,7 @@ cli({
|
|
|
29
29
|
strategy: Strategy.COOKIE,
|
|
30
30
|
browser: true,
|
|
31
31
|
args: [
|
|
32
|
-
{ name: 'id', type: 'string', required: true, help: 'User id or profile URL' },
|
|
32
|
+
{ name: 'id', type: 'string', required: true, positional: true, help: 'User id or profile URL' },
|
|
33
33
|
{ name: 'limit', type: 'int', default: 15, help: 'Number of notes to return' },
|
|
34
34
|
],
|
|
35
35
|
columns: ['id', 'title', 'type', 'likes', 'url'],
|
|
@@ -38,7 +38,6 @@ cli({
|
|
|
38
38
|
const limit = Math.max(1, Number(kwargs.limit ?? 15));
|
|
39
39
|
|
|
40
40
|
await page.goto(`https://www.xiaohongshu.com/user/profile/${userId}`);
|
|
41
|
-
await page.wait(3);
|
|
42
41
|
|
|
43
42
|
let snapshot = await readUserSnapshot(page);
|
|
44
43
|
let results = extractXhsUserNotes(snapshot ?? {}, userId);
|
|
@@ -17,7 +17,6 @@ cli({
|
|
|
17
17
|
func: async (page, kwargs) => {
|
|
18
18
|
const symbol = kwargs.symbol.toUpperCase().trim();
|
|
19
19
|
await page.goto(`https://finance.yahoo.com/quote/${encodeURIComponent(symbol)}/`);
|
|
20
|
-
await page.wait(3);
|
|
21
20
|
const data = await page.evaluate(`
|
|
22
21
|
(async () => {
|
|
23
22
|
const sym = '${symbol}';
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
domain: 'www.youtube.com',
|
|
12
12
|
strategy: Strategy.COOKIE,
|
|
13
13
|
args: [
|
|
14
|
-
{ name: 'query', required: true, help: 'Search query' },
|
|
14
|
+
{ name: 'query', required: true, positional: true, help: 'Search query' },
|
|
15
15
|
{ name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
|
|
16
16
|
],
|
|
17
17
|
columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'],
|
|
@@ -25,7 +25,7 @@ cli({
|
|
|
25
25
|
domain: 'www.youtube.com',
|
|
26
26
|
strategy: Strategy.COOKIE,
|
|
27
27
|
args: [
|
|
28
|
-
{ name: 'url', required: true, help: 'YouTube video URL or video ID' },
|
|
28
|
+
{ name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' },
|
|
29
29
|
{ name: 'lang', required: false, help: 'Language code (e.g. en, zh-Hans). Omit to auto-select' },
|
|
30
30
|
{ name: 'mode', required: false, default: 'grouped', help: 'Output mode: grouped (readable paragraphs) or raw (every segment)' },
|
|
31
31
|
],
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
domain: 'www.youtube.com',
|
|
12
12
|
strategy: Strategy.COOKIE,
|
|
13
13
|
args: [
|
|
14
|
-
{ name: 'url', required: true, help: 'YouTube video URL or video ID' },
|
|
14
|
+
{ name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' },
|
|
15
15
|
],
|
|
16
16
|
columns: ['field', 'value'],
|
|
17
17
|
func: async (page, kwargs) => {
|
|
@@ -92,7 +92,7 @@ cli({
|
|
|
92
92
|
domain: 'zhuanlan.zhihu.com',
|
|
93
93
|
strategy: Strategy.COOKIE,
|
|
94
94
|
args: [
|
|
95
|
-
{ name: 'url', required: true, help: 'Article URL (zhuanlan.zhihu.com/p/xxx)' },
|
|
95
|
+
{ name: 'url', required: true, positional: true, help: 'Article URL (zhuanlan.zhihu.com/p/xxx)' },
|
|
96
96
|
{ name: 'output', default: './zhihu-articles', help: 'Output directory' },
|
|
97
97
|
{ name: 'download-images', type: 'boolean', default: false, help: 'Download images locally' },
|
|
98
98
|
],
|
|
@@ -104,7 +104,6 @@ cli({
|
|
|
104
104
|
|
|
105
105
|
// Navigate to article page
|
|
106
106
|
await page.goto(url);
|
|
107
|
-
await page.wait(3);
|
|
108
107
|
|
|
109
108
|
// Extract article content
|
|
110
109
|
const data = await page.evaluate(`
|
|
@@ -7,7 +7,7 @@ cli({
|
|
|
7
7
|
domain: 'www.zhihu.com',
|
|
8
8
|
strategy: Strategy.COOKIE,
|
|
9
9
|
args: [
|
|
10
|
-
{ name: 'id', required: true, help: 'Question ID (numeric)' },
|
|
10
|
+
{ name: 'id', required: true, positional: true, help: 'Question ID (numeric)' },
|
|
11
11
|
{ name: 'limit', type: 'int', default: 5, help: 'Number of answers' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['rank', 'author', 'votes', 'content'],
|
|
@@ -4,10 +4,11 @@ description: 知乎搜索
|
|
|
4
4
|
domain: www.zhihu.com
|
|
5
5
|
|
|
6
6
|
args:
|
|
7
|
-
|
|
7
|
+
query:
|
|
8
|
+
positional: true
|
|
8
9
|
type: str
|
|
9
10
|
required: true
|
|
10
|
-
description: Search
|
|
11
|
+
description: Search query
|
|
11
12
|
limit:
|
|
12
13
|
type: int
|
|
13
14
|
default: 10
|
|
@@ -19,7 +20,7 @@ pipeline:
|
|
|
19
20
|
- evaluate: |
|
|
20
21
|
(async () => {
|
|
21
22
|
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/<em>/g, '').replace(/<\/em>/g, '').trim();
|
|
22
|
-
const keyword = ${{ args.
|
|
23
|
+
const keyword = ${{ args.query | json }};
|
|
23
24
|
const limit = ${{ args.limit }};
|
|
24
25
|
const res = await fetch('https://www.zhihu.com/api/v4/search_v3?q=' + encodeURIComponent(keyword) + '&t=general&offset=0&limit=' + limit, {
|
|
25
26
|
credentials: 'include'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander adapter: bridges Registry commands to Commander subcommands.
|
|
3
|
+
*
|
|
4
|
+
* This is a THIN adapter — it only handles:
|
|
5
|
+
* 1. Commander arg/option registration
|
|
6
|
+
* 2. Collecting kwargs from Commander's action args
|
|
7
|
+
* 3. Calling executeCommand (which handles browser sessions, validation, etc.)
|
|
8
|
+
* 4. Rendering output and errors
|
|
9
|
+
*
|
|
10
|
+
* All execution logic lives in execution.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { type CliCommand, fullName, getRegistry } from './registry.js';
|
|
16
|
+
import { formatRegistryHelpText } from './serialization.js';
|
|
17
|
+
import { render as renderOutput } from './output.js';
|
|
18
|
+
import { executeCommand } from './execution.js';
|
|
19
|
+
import { CliError } from './errors.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a single CliCommand as a Commander subcommand.
|
|
23
|
+
*/
|
|
24
|
+
export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): void {
|
|
25
|
+
if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) return;
|
|
26
|
+
|
|
27
|
+
const subCmd = siteCmd.command(cmd.name).description(cmd.description);
|
|
28
|
+
|
|
29
|
+
// Register positional args first, then named options
|
|
30
|
+
const positionalArgs: typeof cmd.args = [];
|
|
31
|
+
for (const arg of cmd.args) {
|
|
32
|
+
if (arg.positional) {
|
|
33
|
+
const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
|
|
34
|
+
subCmd.argument(bracket, arg.help ?? '');
|
|
35
|
+
positionalArgs.push(arg);
|
|
36
|
+
} else {
|
|
37
|
+
const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
38
|
+
if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
|
|
39
|
+
else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
40
|
+
else subCmd.option(flag, arg.help ?? '');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
subCmd
|
|
44
|
+
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
45
|
+
.option('-v, --verbose', 'Debug output', false);
|
|
46
|
+
|
|
47
|
+
subCmd.addHelpText('after', formatRegistryHelpText(cmd));
|
|
48
|
+
|
|
49
|
+
subCmd.action(async (...actionArgs: any[]) => {
|
|
50
|
+
const actionOpts = actionArgs[positionalArgs.length] ?? {};
|
|
51
|
+
const startTime = Date.now();
|
|
52
|
+
|
|
53
|
+
// ── Collect kwargs ──────────────────────────────────────────────────
|
|
54
|
+
const kwargs: Record<string, any> = {};
|
|
55
|
+
for (let i = 0; i < positionalArgs.length; i++) {
|
|
56
|
+
const v = actionArgs[i];
|
|
57
|
+
if (v !== undefined) kwargs[positionalArgs[i].name] = v;
|
|
58
|
+
}
|
|
59
|
+
for (const arg of cmd.args) {
|
|
60
|
+
if (arg.positional) continue;
|
|
61
|
+
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
|
|
62
|
+
const v = actionOpts[arg.name] ?? actionOpts[camelName];
|
|
63
|
+
if (v !== undefined) kwargs[arg.name] = v;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Execute + render ────────────────────────────────────────────────
|
|
67
|
+
try {
|
|
68
|
+
if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
69
|
+
|
|
70
|
+
const result = await executeCommand(cmd, kwargs, actionOpts.verbose);
|
|
71
|
+
|
|
72
|
+
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
73
|
+
console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
|
|
74
|
+
}
|
|
75
|
+
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
76
|
+
renderOutput(result, {
|
|
77
|
+
fmt: actionOpts.format,
|
|
78
|
+
columns: resolved.columns,
|
|
79
|
+
title: `${resolved.site}/${resolved.name}`,
|
|
80
|
+
elapsed: (Date.now() - startTime) / 1000,
|
|
81
|
+
source: fullName(resolved),
|
|
82
|
+
footerExtra: resolved.footerExtra?.(kwargs),
|
|
83
|
+
});
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
if (err instanceof CliError) {
|
|
86
|
+
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
|
|
87
|
+
if (err.hint) console.error(chalk.yellow(`Hint: ${err.hint}`));
|
|
88
|
+
} else if (actionOpts.verbose && err.stack) {
|
|
89
|
+
console.error(chalk.red(err.stack));
|
|
90
|
+
} else {
|
|
91
|
+
console.error(chalk.red(`Error: ${err.message ?? err}`));
|
|
92
|
+
}
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Register all commands from the registry onto a Commander program.
|
|
100
|
+
*/
|
|
101
|
+
export function registerAllCommands(
|
|
102
|
+
program: Command,
|
|
103
|
+
siteGroups: Map<string, Command>,
|
|
104
|
+
): void {
|
|
105
|
+
for (const [, cmd] of getRegistry()) {
|
|
106
|
+
let siteCmd = siteGroups.get(cmd.site);
|
|
107
|
+
if (!siteCmd) {
|
|
108
|
+
siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
|
|
109
|
+
siteGroups.set(cmd.site, siteCmd);
|
|
110
|
+
}
|
|
111
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
112
|
+
}
|
|
113
|
+
}
|