feat(core): support YAML format output and optimize manifest builder

This commit is contained in:
jackwener committed 2026-03-15 21:05:36 +08:00
1 parent eb9050fadd
commit ee6669f45d
7 files changed
+70 -7

No files matched your search

+6 -1
View File
@@ -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
+6 -1
View File
@@ -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 # 详细模式:展示管线执行步骤调试信息
+5 -1
View File
@@ -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 <site/name> --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
```
+1 -1
View File
@@ -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",
+4
View File
@@ -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) {
+21 -3
View File
@@ -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 <fmt>', '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<string, CliCommand[]>();
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 <fmt>', 'Output format: table, json, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
subCmd.action(async (actionOpts) => {
const startTime = Date.now();
+27
View File
@@ -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');
});
});