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