From ee6669f45dda2e663533f79683673f10162639e8 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 15 Mar 2026 21:05:36 +0800 Subject: [PATCH] feat(core): support YAML format output and optimize manifest builder --- README.md | 7 ++++++- README.zh-CN.md | 7 ++++++- SKILL.md | 6 +++++- package-lock.json | 2 +- src/build-manifest.ts | 4 ++++ src/main.ts | 24 +++++++++++++++++++++--- src/output.test.ts | 27 +++++++++++++++++++++++++++ 7 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/output.test.ts diff --git a/README.md b/README.md index 30d96b2..e9c3904 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,11 @@ Then use directly: ```bash opencli list # See all commands +opencli list -f yaml # List commands as YAML opencli hackernews top --limit 5 # Public API, no browser opencli bilibili hot --limit 5 # Browser command opencli zhihu hot -f json # JSON output +opencli zhihu hot -f yaml # YAML output ``` ### Install from source (for developers) @@ -140,11 +142,14 @@ npm install -g @jackwener/opencli@latest ## Output Formats -Commands support various format outputs: +All built-in commands support `--format` / `-f` with `table`, `json`, `yaml`, `md`, and `csv`. +The `list` command supports the same format options, and keeps `--json` for backward compatibility. ```bash +opencli list -f yaml # Command registry as YAML opencli bilibili hot -f table # Default: rich terminal table opencli bilibili hot -f json # JSON (pipe to jq or LLMs) +opencli bilibili hot -f yaml # YAML (human-readable structured output) opencli bilibili hot -f md # Markdown opencli bilibili hot -f csv # CSV opencli bilibili hot -v # Verbose: show pipeline debug steps diff --git a/README.zh-CN.md b/README.zh-CN.md index 3d1dabe..6f7e33b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -94,9 +94,11 @@ npm install -g @jackwener/opencli ```bash opencli list # 查看所有命令 +opencli list -f yaml # 以 YAML 列出所有命令 opencli hackernews top --limit 5 # 公共 API,无需浏览器 opencli bilibili hot --limit 5 # 浏览器命令 opencli zhihu hot -f json # JSON 输出 +opencli zhihu hot -f yaml # YAML 输出 ``` ### 从源码安装(面向开发者) @@ -140,11 +142,14 @@ npm install -g @jackwener/opencli@latest ## 输出格式 -所有的命令都支持这些结果格式控制参数: +所有内置命令都支持 `--format` / `-f`,可选值为 `table`、`json`、`yaml`、`md`、`csv`。 +`list` 命令也支持同样的格式参数,同时继续兼容 `--json`。 ```bash +opencli list -f yaml # 用 YAML 列出命令注册表 opencli bilibili hot -f table # 默认:富文本表格 opencli bilibili hot -f json # JSON(适合传给 jq 或者各类 AI Agent) +opencli bilibili hot -f yaml # YAML(更适合人类直接阅读) opencli bilibili hot -f md # Markdown opencli bilibili hot -f csv # CSV opencli bilibili hot -v # 详细模式:展示管线执行步骤调试信息 diff --git a/SKILL.md b/SKILL.md index 59d5a39..de090af 100644 --- a/SKILL.md +++ b/SKILL.md @@ -134,6 +134,7 @@ opencli ctrip search --query "三亚" # 搜索目的地 ```bash opencli list # List all commands opencli list --json # JSON output +opencli list -f yaml # YAML output opencli validate # Validate all CLI definitions opencli validate bilibili # Validate specific site ``` @@ -162,11 +163,14 @@ opencli verify --smoke ## Output Formats -All commands support `--format` / `-f`: +All built-in commands support `--format` / `-f` with `table`, `json`, `yaml`, `md`, and `csv`. +The `list` command supports the same formats and also keeps `--json` as a compatibility alias. ```bash +opencli list -f yaml # YAML command registry opencli bilibili hot -f table # Default: rich table opencli bilibili hot -f json # JSON (pipe to jq, feed to AI agent) +opencli bilibili hot -f yaml # YAML (readable structured output) opencli bilibili hot -f md # Markdown opencli bilibili hot -f csv # CSV ``` diff --git a/package-lock.json b/package-lock.json index ce9e60c..64879ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "@jackwener/opencli", "version": "0.4.2", - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { "chalk": "^5.3.0", "cli-table3": "^0.6.5", diff --git a/src/build-manifest.ts b/src/build-manifest.ts index edf9e69..882fb30 100644 --- a/src/build-manifest.ts +++ b/src/build-manifest.ts @@ -117,6 +117,10 @@ function scanTs(filePath: string, site: string): ManifestEntry { const stratMatch = src.match(/strategy\s*:\s*Strategy\.(\w+)/); if (stratMatch) entry.strategy = stratMatch[1].toLowerCase(); + // Extract browser: false (some adapters bypass browser entirely) + const browserMatch = src.match(/browser\s*:\s*(true|false)/); + if (browserMatch) entry.browser = browserMatch[1] === 'true'; + // Extract columns const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/); if (colMatch) { diff --git a/src/main.ts b/src/main.ts index b10937b..be2aaa0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,11 +31,29 @@ program.name('opencli').description('Make any website your CLI. Zero setup. AI-p // ── Built-in commands ────────────────────────────────────────────────────── -program.command('list').description('List all available CLI commands').option('--json', 'JSON output') +program.command('list').description('List all available CLI commands').option('-f, --format ', 'Output format: table, json, yaml, md, csv', 'table').option('--json', 'JSON output (deprecated)') .action((opts) => { const registry = getRegistry(); const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b))); - if (opts.json) { console.log(JSON.stringify(commands.map(c => ({ command: fullName(c), site: c.site, name: c.name, description: c.description, strategy: strategyLabel(c), browser: c.browser, args: c.args.map(a => a.name) })), null, 2)); return; } + const rows = commands.map(c => ({ + command: fullName(c), + site: c.site, + name: c.name, + description: c.description, + strategy: strategyLabel(c), + browser: c.browser, + args: c.args.map(a => a.name).join(', '), + })); + const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format; + if (fmt !== 'table') { + renderOutput(rows, { + fmt, + columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'], + title: 'opencli/list', + source: 'opencli list', + }); + return; + } const sites = new Map(); for (const cmd of commands) { const g = sites.get(cmd.site) ?? []; g.push(cmd); sites.set(cmd.site, g); } console.log(); console.log(chalk.bold(' opencli') + chalk.dim(' — available commands')); console.log(); @@ -89,7 +107,7 @@ for (const [, cmd] of registry) { else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default)); else subCmd.option(flag, arg.help ?? ''); } - subCmd.option('-f, --format ', 'Output format: table, json, md, csv', 'table').option('-v, --verbose', 'Debug output', false); + subCmd.option('-f, --format ', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false); subCmd.action(async (actionOpts) => { const startTime = Date.now(); diff --git a/src/output.test.ts b/src/output.test.ts new file mode 100644 index 0000000..53d7d76 --- /dev/null +++ b/src/output.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render } from './output.js'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('render', () => { + it('renders YAML output', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + + render([{ title: 'Hello', rank: 1 }], { fmt: 'yaml' }); + + expect(log).toHaveBeenCalledOnce(); + expect(log.mock.calls[0]?.[0]).toContain('- title: Hello'); + expect(log.mock.calls[0]?.[0]).toContain('rank: 1'); + }); + + it('renders yml alias as YAML output', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + + render({ title: 'Hello' }, { fmt: 'yml' }); + + expect(log).toHaveBeenCalledOnce(); + expect(log.mock.calls[0]?.[0]).toContain('title: Hello'); + }); +});