@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
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point: registers built-in commands and wires up Commander.
|
|
3
|
+
*
|
|
4
|
+
* Built-in commands are registered inline here (list, validate, explore, etc.).
|
|
5
|
+
* Dynamic adapter commands are registered via commanderAdapter.ts.
|
|
6
|
+
*/
|
|
1
7
|
export declare function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void;
|
package/dist/cli.js
CHANGED
|
@@ -1,42 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point: registers built-in commands and wires up Commander.
|
|
3
|
+
*
|
|
4
|
+
* Built-in commands are registered inline here (list, validate, explore, etc.).
|
|
5
|
+
* Dynamic adapter commands are registered via commanderAdapter.ts.
|
|
6
|
+
*/
|
|
1
7
|
import { Command } from 'commander';
|
|
2
8
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
9
|
+
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
10
|
+
import { serializeCommand, formatArgSummary } from './serialization.js';
|
|
5
11
|
import { render as renderOutput } from './output.js';
|
|
6
|
-
import {
|
|
7
|
-
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
12
|
+
import { getBrowserFactory, browserSession } from './runtime.js';
|
|
8
13
|
import { PKG_VERSION } from './version.js';
|
|
9
14
|
import { printCompletionScript } from './completion.js';
|
|
10
|
-
import { CliError } from './errors.js';
|
|
11
|
-
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
12
15
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
|
|
16
|
+
import { registerAllCommands } from './commanderAdapter.js';
|
|
13
17
|
export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
14
18
|
const program = new Command();
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
program
|
|
19
|
+
// enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
|
|
20
|
+
// prerequisite for passThroughOptions to forward --help/--version to external binaries
|
|
21
|
+
program
|
|
22
|
+
.name('opencli')
|
|
23
|
+
.description('Make any website your CLI. Zero setup. AI-powered.')
|
|
24
|
+
.version(PKG_VERSION)
|
|
25
|
+
.enablePositionalOptions();
|
|
26
|
+
// ── Built-in: list ────────────────────────────────────────────────────────
|
|
27
|
+
program
|
|
28
|
+
.command('list')
|
|
29
|
+
.description('List all available CLI commands')
|
|
30
|
+
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
31
|
+
.option('--json', 'JSON output (deprecated)')
|
|
18
32
|
.action((opts) => {
|
|
19
33
|
const registry = getRegistry();
|
|
20
34
|
const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b)));
|
|
21
|
-
const rows = commands.map(c => ({
|
|
22
|
-
command: fullName(c),
|
|
23
|
-
site: c.site,
|
|
24
|
-
name: c.name,
|
|
25
|
-
description: c.description,
|
|
26
|
-
strategy: strategyLabel(c),
|
|
27
|
-
browser: c.browser,
|
|
28
|
-
args: c.args.map(a => a.name).join(', '),
|
|
29
|
-
}));
|
|
30
35
|
const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format;
|
|
36
|
+
const isStructured = fmt === 'json' || fmt === 'yaml';
|
|
31
37
|
if (fmt !== 'table') {
|
|
38
|
+
const rows = isStructured
|
|
39
|
+
? commands.map(serializeCommand)
|
|
40
|
+
: commands.map(c => ({
|
|
41
|
+
command: fullName(c),
|
|
42
|
+
site: c.site,
|
|
43
|
+
name: c.name,
|
|
44
|
+
description: c.description,
|
|
45
|
+
strategy: strategyLabel(c),
|
|
46
|
+
browser: !!c.browser,
|
|
47
|
+
args: formatArgSummary(c.args),
|
|
48
|
+
}));
|
|
32
49
|
renderOutput(rows, {
|
|
33
50
|
fmt,
|
|
34
|
-
columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'
|
|
51
|
+
columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args',
|
|
52
|
+
...(isStructured ? ['columns', 'domain'] : [])],
|
|
35
53
|
title: 'opencli/list',
|
|
36
54
|
source: 'opencli list',
|
|
37
55
|
});
|
|
38
56
|
return;
|
|
39
57
|
}
|
|
58
|
+
// Table (default) — grouped by site
|
|
40
59
|
const sites = new Map();
|
|
41
60
|
for (const cmd of commands) {
|
|
42
61
|
const g = sites.get(cmd.site) ?? [];
|
|
@@ -49,14 +68,16 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
49
68
|
for (const [site, cmds] of sites) {
|
|
50
69
|
console.log(chalk.bold.cyan(` ${site}`));
|
|
51
70
|
for (const cmd of cmds) {
|
|
52
|
-
const tag = strategyLabel(cmd) === 'public'
|
|
71
|
+
const tag = strategyLabel(cmd) === 'public'
|
|
72
|
+
? chalk.green('[public]')
|
|
73
|
+
: chalk.yellow(`[${strategyLabel(cmd)}]`);
|
|
53
74
|
console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`);
|
|
54
75
|
}
|
|
55
76
|
console.log();
|
|
56
77
|
}
|
|
57
78
|
const externalClis = loadExternalClis();
|
|
58
79
|
if (externalClis.length > 0) {
|
|
59
|
-
console.log(chalk.bold.cyan(
|
|
80
|
+
console.log(chalk.bold.cyan(' external CLIs'));
|
|
60
81
|
for (const ext of externalClis) {
|
|
61
82
|
const isInstalled = isBinaryInstalled(ext.binary);
|
|
62
83
|
const tag = isInstalled ? chalk.green('[installed]') : chalk.yellow('[auto-install]');
|
|
@@ -67,40 +88,93 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
67
88
|
console.log(chalk.dim(` ${commands.length} built-in commands across ${sites.size} sites, ${externalClis.length} external CLIs`));
|
|
68
89
|
console.log();
|
|
69
90
|
});
|
|
70
|
-
|
|
91
|
+
// ── Built-in: validate / verify ───────────────────────────────────────────
|
|
92
|
+
program
|
|
93
|
+
.command('validate')
|
|
94
|
+
.description('Validate CLI definitions')
|
|
95
|
+
.argument('[target]', 'site or site/name')
|
|
71
96
|
.action(async (target) => {
|
|
72
97
|
const { validateClisWithTarget, renderValidationReport } = await import('./validate.js');
|
|
73
98
|
console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target)));
|
|
74
99
|
});
|
|
75
|
-
program
|
|
100
|
+
program
|
|
101
|
+
.command('verify')
|
|
102
|
+
.description('Validate + smoke test')
|
|
103
|
+
.argument('[target]')
|
|
104
|
+
.option('--smoke', 'Run smoke tests', false)
|
|
76
105
|
.action(async (target, opts) => {
|
|
77
106
|
const { verifyClis, renderVerifyReport } = await import('./verify.js');
|
|
78
107
|
const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
|
|
79
108
|
console.log(renderVerifyReport(r));
|
|
80
109
|
process.exitCode = r.ok ? 0 : 1;
|
|
81
110
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
// ── Built-in: explore / synthesize / generate / cascade ───────────────────
|
|
112
|
+
program
|
|
113
|
+
.command('explore')
|
|
114
|
+
.alias('probe')
|
|
115
|
+
.description('Explore a website: discover APIs, stores, and recommend strategies')
|
|
116
|
+
.argument('<url>')
|
|
117
|
+
.option('--site <name>')
|
|
118
|
+
.option('--goal <text>')
|
|
119
|
+
.option('--wait <s>', '', '3')
|
|
120
|
+
.option('--auto', 'Enable interactive fuzzing')
|
|
121
|
+
.option('--click <labels>', 'Comma-separated labels to click before fuzzing')
|
|
122
|
+
.action(async (url, opts) => {
|
|
123
|
+
const { exploreUrl, renderExploreSummary } = await import('./explore.js');
|
|
124
|
+
const clickLabels = opts.click
|
|
125
|
+
? opts.click.split(',').map((s) => s.trim())
|
|
126
|
+
: undefined;
|
|
127
|
+
const workspace = `explore:${inferHost(url, opts.site)}`;
|
|
128
|
+
const result = await exploreUrl(url, {
|
|
129
|
+
BrowserFactory: getBrowserFactory(),
|
|
130
|
+
site: opts.site,
|
|
131
|
+
goal: opts.goal,
|
|
132
|
+
waitSeconds: parseFloat(opts.wait),
|
|
133
|
+
auto: opts.auto,
|
|
134
|
+
clickLabels,
|
|
135
|
+
workspace,
|
|
136
|
+
});
|
|
137
|
+
console.log(renderExploreSummary(result));
|
|
138
|
+
});
|
|
139
|
+
program
|
|
140
|
+
.command('synthesize')
|
|
141
|
+
.description('Synthesize CLIs from explore')
|
|
142
|
+
.argument('<target>')
|
|
143
|
+
.option('--top <n>', '', '3')
|
|
144
|
+
.action(async (target, opts) => {
|
|
145
|
+
const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js');
|
|
146
|
+
console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) })));
|
|
147
|
+
});
|
|
148
|
+
program
|
|
149
|
+
.command('generate')
|
|
150
|
+
.description('One-shot: explore → synthesize → register')
|
|
151
|
+
.argument('<url>')
|
|
152
|
+
.option('--goal <text>')
|
|
153
|
+
.option('--site <name>')
|
|
154
|
+
.action(async (url, opts) => {
|
|
155
|
+
const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
|
|
156
|
+
const workspace = `generate:${inferHost(url, opts.site)}`;
|
|
157
|
+
const r = await generateCliFromUrl({
|
|
158
|
+
url,
|
|
159
|
+
BrowserFactory: getBrowserFactory(),
|
|
160
|
+
builtinClis: BUILTIN_CLIS,
|
|
161
|
+
userClis: USER_CLIS,
|
|
162
|
+
goal: opts.goal,
|
|
163
|
+
site: opts.site,
|
|
164
|
+
workspace,
|
|
165
|
+
});
|
|
166
|
+
console.log(renderGenerateSummary(r));
|
|
167
|
+
process.exitCode = r.ok ? 0 : 1;
|
|
168
|
+
});
|
|
169
|
+
program
|
|
170
|
+
.command('cascade')
|
|
171
|
+
.description('Strategy cascade: find simplest working strategy')
|
|
172
|
+
.argument('<url>')
|
|
173
|
+
.option('--site <name>')
|
|
99
174
|
.action(async (url, opts) => {
|
|
100
175
|
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
|
|
101
|
-
const
|
|
102
|
-
const result = await browserSession(
|
|
103
|
-
// Navigate to the site first for cookie context
|
|
176
|
+
const workspace = `cascade:${inferHost(url, opts.site)}`;
|
|
177
|
+
const result = await browserSession(getBrowserFactory(), async (page) => {
|
|
104
178
|
try {
|
|
105
179
|
const siteUrl = new URL(url);
|
|
106
180
|
await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
|
|
@@ -108,15 +182,12 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
108
182
|
}
|
|
109
183
|
catch { }
|
|
110
184
|
return cascadeProbe(page, url);
|
|
111
|
-
}, { workspace
|
|
112
|
-
return new URL(url).host;
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return 'default';
|
|
116
|
-
} })()}` });
|
|
185
|
+
}, { workspace });
|
|
117
186
|
console.log(renderCascadeResult(result));
|
|
118
187
|
});
|
|
119
|
-
|
|
188
|
+
// ── Built-in: doctor / setup / completion ─────────────────────────────────
|
|
189
|
+
program
|
|
190
|
+
.command('doctor')
|
|
120
191
|
.description('Diagnose opencli browser bridge connectivity')
|
|
121
192
|
.option('--live', 'Test browser connectivity (requires Chrome running)', false)
|
|
122
193
|
.option('--sessions', 'Show active automation sessions', false)
|
|
@@ -125,20 +196,24 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
125
196
|
const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
|
|
126
197
|
console.log(renderBrowserDoctorReport(report));
|
|
127
198
|
});
|
|
128
|
-
program
|
|
199
|
+
program
|
|
200
|
+
.command('setup')
|
|
129
201
|
.description('Interactive setup: verify browser bridge connectivity')
|
|
130
202
|
.action(async () => {
|
|
131
203
|
const { runSetup } = await import('./setup.js');
|
|
132
204
|
await runSetup({ cliVersion: PKG_VERSION });
|
|
133
205
|
});
|
|
134
|
-
program
|
|
206
|
+
program
|
|
207
|
+
.command('completion')
|
|
135
208
|
.description('Output shell completion script')
|
|
136
209
|
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
137
210
|
.action((shell) => {
|
|
138
211
|
printCompletionScript(shell);
|
|
139
212
|
});
|
|
213
|
+
// ── External CLIs ─────────────────────────────────────────────────────────
|
|
140
214
|
const externalClis = loadExternalClis();
|
|
141
|
-
program
|
|
215
|
+
program
|
|
216
|
+
.command('install')
|
|
142
217
|
.description('Install an external CLI')
|
|
143
218
|
.argument('<name>', 'Name of the external CLI')
|
|
144
219
|
.action((name) => {
|
|
@@ -150,138 +225,90 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
150
225
|
}
|
|
151
226
|
installExternalCli(ext);
|
|
152
227
|
});
|
|
153
|
-
program
|
|
228
|
+
program
|
|
229
|
+
.command('register')
|
|
154
230
|
.description('Register an external CLI')
|
|
155
231
|
.argument('<name>', 'Name of the CLI')
|
|
156
232
|
.option('--binary <bin>', 'Binary name if different from name')
|
|
157
233
|
.option('--install <cmd>', 'Auto-install command')
|
|
158
234
|
.option('--desc <text>', 'Description')
|
|
159
235
|
.action((name, opts) => {
|
|
160
|
-
registerExternalCli(name, opts.binary, opts.install, opts.desc);
|
|
236
|
+
registerExternalCli(name, { binary: opts.binary, install: opts.install, description: opts.desc });
|
|
161
237
|
});
|
|
238
|
+
function passthroughExternal(name, parsedArgs) {
|
|
239
|
+
const args = parsedArgs ?? (() => {
|
|
240
|
+
const idx = process.argv.indexOf(name);
|
|
241
|
+
return process.argv.slice(idx + 1);
|
|
242
|
+
})();
|
|
243
|
+
try {
|
|
244
|
+
executeExternalCli(name, args, externalClis);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
162
251
|
for (const ext of externalClis) {
|
|
163
252
|
if (program.commands.some(c => c.name() === ext.name))
|
|
164
253
|
continue;
|
|
165
|
-
program
|
|
254
|
+
program
|
|
255
|
+
.command(ext.name)
|
|
166
256
|
.description(`(External) ${ext.description || ext.name}`)
|
|
257
|
+
.argument('[args...]')
|
|
167
258
|
.allowUnknownOption()
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const extIndex = process.argv.indexOf(ext.name);
|
|
172
|
-
const args = process.argv.slice(extIndex + 1);
|
|
173
|
-
executeExternalCli(ext.name, args).catch(err => {
|
|
174
|
-
console.error(chalk.red(`Error: ${err.message}`));
|
|
175
|
-
process.exitCode = 1;
|
|
176
|
-
});
|
|
177
|
-
});
|
|
259
|
+
.passThroughOptions()
|
|
260
|
+
.helpOption(false)
|
|
261
|
+
.action((args) => passthroughExternal(ext.name, args));
|
|
178
262
|
}
|
|
179
|
-
// ── Antigravity serve (
|
|
263
|
+
// ── Antigravity serve (long-running, special case) ────────────────────────
|
|
180
264
|
const antigravityCmd = program.command('antigravity').description('antigravity commands');
|
|
181
|
-
antigravityCmd
|
|
265
|
+
antigravityCmd
|
|
266
|
+
.command('serve')
|
|
182
267
|
.description('Start Anthropic-compatible API proxy for Antigravity')
|
|
183
268
|
.option('--port <port>', 'Server port (default: 8082)', '8082')
|
|
184
269
|
.action(async (opts) => {
|
|
185
270
|
const { startServe } = await import('./clis/antigravity/serve.js');
|
|
186
271
|
await startServe({ port: parseInt(opts.port) });
|
|
187
272
|
});
|
|
188
|
-
// ── Dynamic
|
|
189
|
-
const registry = getRegistry();
|
|
273
|
+
// ── Dynamic adapter commands ──────────────────────────────────────────────
|
|
190
274
|
const siteGroups = new Map();
|
|
191
|
-
// Pre-seed with the antigravity command registered above to avoid duplicates
|
|
192
275
|
siteGroups.set('antigravity', antigravityCmd);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
276
|
+
registerAllCommands(program, siteGroups);
|
|
277
|
+
// ── Unknown command fallback ──────────────────────────────────────────────
|
|
278
|
+
const DENY_LIST = new Set([
|
|
279
|
+
'rm', 'sudo', 'dd', 'mkfs', 'fdisk', 'shutdown', 'reboot',
|
|
280
|
+
'kill', 'killall', 'chmod', 'chown', 'passwd', 'su', 'mount',
|
|
281
|
+
'umount', 'format', 'diskutil',
|
|
282
|
+
]);
|
|
283
|
+
program.on('command:*', (operands) => {
|
|
284
|
+
const binary = operands[0];
|
|
285
|
+
if (DENY_LIST.has(binary)) {
|
|
286
|
+
console.error(chalk.red(`Refusing to register system command '${binary}'.`));
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
return;
|
|
198
289
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Register positional args first, then named options
|
|
204
|
-
const positionalArgs = [];
|
|
205
|
-
for (const arg of cmd.args) {
|
|
206
|
-
if (arg.positional) {
|
|
207
|
-
const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
|
|
208
|
-
subCmd.argument(bracket, arg.help ?? '');
|
|
209
|
-
positionalArgs.push(arg);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
213
|
-
if (arg.required)
|
|
214
|
-
subCmd.requiredOption(flag, arg.help ?? '');
|
|
215
|
-
else if (arg.default != null)
|
|
216
|
-
subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
217
|
-
else
|
|
218
|
-
subCmd.option(flag, arg.help ?? '');
|
|
219
|
-
}
|
|
290
|
+
if (isBinaryInstalled(binary)) {
|
|
291
|
+
console.log(chalk.cyan(`🔹 Auto-discovered local CLI '${binary}'. Registering...`));
|
|
292
|
+
registerExternalCli(binary);
|
|
293
|
+
passthroughExternal(binary);
|
|
220
294
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Collect positional args
|
|
228
|
-
for (let i = 0; i < positionalArgs.length; i++) {
|
|
229
|
-
const arg = positionalArgs[i];
|
|
230
|
-
const v = actionArgs[i];
|
|
231
|
-
if (v !== undefined)
|
|
232
|
-
kwargs[arg.name] = v;
|
|
233
|
-
}
|
|
234
|
-
// Collect named options
|
|
235
|
-
for (const arg of cmd.args) {
|
|
236
|
-
if (arg.positional)
|
|
237
|
-
continue;
|
|
238
|
-
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
|
|
239
|
-
const v = actionOpts[arg.name] ?? actionOpts[camelName];
|
|
240
|
-
if (v !== undefined)
|
|
241
|
-
kwargs[arg.name] = v;
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
if (actionOpts.verbose)
|
|
245
|
-
process.env.OPENCLI_VERBOSE = '1';
|
|
246
|
-
let result;
|
|
247
|
-
if (shouldUseBrowserSession(cmd)) {
|
|
248
|
-
const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
|
|
249
|
-
result = await browserSession(BrowserFactory, async (page) => {
|
|
250
|
-
// Cookie/header strategies require same-origin context for credentialed fetch.
|
|
251
|
-
if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
|
|
252
|
-
try {
|
|
253
|
-
await page.goto(`https://${cmd.domain}`);
|
|
254
|
-
await page.wait(2);
|
|
255
|
-
}
|
|
256
|
-
catch { }
|
|
257
|
-
}
|
|
258
|
-
return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
|
|
259
|
-
}, { workspace: `site:${cmd.site}` });
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
|
|
263
|
-
}
|
|
264
|
-
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
265
|
-
console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
|
|
266
|
-
}
|
|
267
|
-
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
268
|
-
renderOutput(result, { fmt: actionOpts.format, columns: resolved.columns, title: `${resolved.site}/${resolved.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(resolved), footerExtra: resolved.footerExtra?.(kwargs) });
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
if (err instanceof CliError) {
|
|
272
|
-
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
|
|
273
|
-
if (err.hint)
|
|
274
|
-
console.error(chalk.yellow(`Hint: ${err.hint}`));
|
|
275
|
-
}
|
|
276
|
-
else if (actionOpts.verbose && err.stack) {
|
|
277
|
-
console.error(chalk.red(err.stack));
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
console.error(chalk.red(`Error: ${err.message ?? err}`));
|
|
281
|
-
}
|
|
282
|
-
process.exitCode = 1;
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
}
|
|
295
|
+
else {
|
|
296
|
+
console.error(chalk.red(`error: unknown command '${binary}'`));
|
|
297
|
+
program.outputHelp();
|
|
298
|
+
process.exitCode = 1;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
286
301
|
program.parse();
|
|
287
302
|
}
|
|
303
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
304
|
+
/** Infer a workspace-friendly hostname from a URL, with site override. */
|
|
305
|
+
function inferHost(url, site) {
|
|
306
|
+
if (site)
|
|
307
|
+
return site;
|
|
308
|
+
try {
|
|
309
|
+
return new URL(url).host;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return 'default';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './search.js';
|
|
4
|
+
import './top.js';
|
|
5
|
+
describe('apple-podcasts search command', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.restoreAllMocks();
|
|
8
|
+
});
|
|
9
|
+
it('uses the positional query argument for the iTunes search request', async () => {
|
|
10
|
+
const cmd = getRegistry().get('apple-podcasts/search');
|
|
11
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
12
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
13
|
+
ok: true,
|
|
14
|
+
json: () => Promise.resolve({
|
|
15
|
+
results: [
|
|
16
|
+
{
|
|
17
|
+
collectionId: 42,
|
|
18
|
+
collectionName: 'Machine Learning Guide',
|
|
19
|
+
artistName: 'OpenCLI',
|
|
20
|
+
trackCount: 12,
|
|
21
|
+
primaryGenreName: 'Technology',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
27
|
+
const result = await cmd.func(null, {
|
|
28
|
+
query: 'machine learning',
|
|
29
|
+
keyword: 'sports',
|
|
30
|
+
limit: 5,
|
|
31
|
+
});
|
|
32
|
+
expect(fetchMock).toHaveBeenCalledWith('https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5');
|
|
33
|
+
expect(result).toEqual([
|
|
34
|
+
{
|
|
35
|
+
id: 42,
|
|
36
|
+
title: 'Machine Learning Guide',
|
|
37
|
+
author: 'OpenCLI',
|
|
38
|
+
episodes: 12,
|
|
39
|
+
genre: 'Technology',
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('apple-podcasts top command', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
});
|
|
48
|
+
it('uses the canonical Apple charts host and maps ranked results', async () => {
|
|
49
|
+
const cmd = getRegistry().get('apple-podcasts/top');
|
|
50
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
51
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
52
|
+
ok: true,
|
|
53
|
+
json: () => Promise.resolve({
|
|
54
|
+
feed: {
|
|
55
|
+
results: [
|
|
56
|
+
{ id: '100', name: 'Top Show', artistName: 'Host A' },
|
|
57
|
+
{ id: '101', name: 'Second Show', artistName: 'Host B' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
63
|
+
const result = await cmd.func(null, { country: 'US', limit: 2 });
|
|
64
|
+
expect(fetchMock).toHaveBeenCalledWith('https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json');
|
|
65
|
+
expect(result).toEqual([
|
|
66
|
+
{ rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
|
|
67
|
+
{ rank: 2, title: 'Second Show', author: 'Host B', id: '101' },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
it('normalizes network failures into CliError output', async () => {
|
|
71
|
+
const cmd = getRegistry().get('apple-podcasts/top');
|
|
72
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
73
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
|
|
74
|
+
await expect(cmd.func(null, { country: 'us', limit: 3 })).rejects.toThrow('Unable to reach Apple Podcasts charts for US');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -8,12 +8,12 @@ cli({
|
|
|
8
8
|
strategy: Strategy.PUBLIC,
|
|
9
9
|
browser: false,
|
|
10
10
|
args: [
|
|
11
|
-
{ name: '
|
|
11
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword' },
|
|
12
12
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['id', 'title', 'author', 'episodes', 'genre'],
|
|
15
15
|
func: async (_page, args) => {
|
|
16
|
-
const term = encodeURIComponent(args.
|
|
16
|
+
const term = encodeURIComponent(args.query);
|
|
17
17
|
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
|
18
18
|
const data = await itunesFetch(`/search?term=${term}&media=podcast&limit=${limit}`);
|
|
19
19
|
if (!data.results?.length)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { CliError } from '../../errors.js';
|
|
3
3
|
// Apple Marketing Tools RSS API — public, no key required
|
|
4
|
-
const CHARTS_URL = 'https://rss.
|
|
4
|
+
const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
|
|
5
5
|
cli({
|
|
6
6
|
site: 'apple-podcasts',
|
|
7
7
|
name: 'top',
|
|
@@ -17,7 +17,14 @@ cli({
|
|
|
17
17
|
const limit = Math.max(1, Math.min(Number(args.limit), 100));
|
|
18
18
|
const country = String(args.country || 'us').trim().toLowerCase();
|
|
19
19
|
const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
|
|
20
|
-
|
|
20
|
+
let resp;
|
|
21
|
+
try {
|
|
22
|
+
resp = await fetch(url);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
|
|
26
|
+
throw new CliError('FETCH_ERROR', `Unable to reach Apple Podcasts charts for ${country.toUpperCase()}`, `Apple charts may be temporarily unavailable (${reason}). Try again later.`);
|
|
27
|
+
}
|
|
21
28
|
if (!resp.ok)
|
|
22
29
|
throw new CliError('FETCH_ERROR', `Charts API HTTP ${resp.status}`, `Check country code: ${country}`);
|
|
23
30
|
const data = await resp.json();
|
|
@@ -8,7 +8,7 @@ cli({
|
|
|
8
8
|
strategy: Strategy.PUBLIC,
|
|
9
9
|
browser: false,
|
|
10
10
|
args: [
|
|
11
|
-
{ name: '
|
|
11
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
|
|
12
12
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['id', 'title', 'authors', 'published'],
|