@jackwener/opencli 0.1.0
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/.github/workflows/ci.yml +26 -0
- package/.github/workflows/release.yml +40 -0
- package/README.md +67 -0
- package/SKILL.md +230 -0
- package/dist/bilibili.d.ts +13 -0
- package/dist/bilibili.js +93 -0
- package/dist/browser.d.ts +48 -0
- package/dist/browser.js +261 -0
- package/dist/clis/bilibili/favorite.d.ts +1 -0
- package/dist/clis/bilibili/favorite.js +39 -0
- package/dist/clis/bilibili/feed.d.ts +1 -0
- package/dist/clis/bilibili/feed.js +64 -0
- package/dist/clis/bilibili/history.d.ts +1 -0
- package/dist/clis/bilibili/history.js +44 -0
- package/dist/clis/bilibili/me.d.ts +1 -0
- package/dist/clis/bilibili/me.js +13 -0
- package/dist/clis/bilibili/search.d.ts +1 -0
- package/dist/clis/bilibili/search.js +24 -0
- package/dist/clis/bilibili/user-videos.d.ts +1 -0
- package/dist/clis/bilibili/user-videos.js +38 -0
- package/dist/clis/github/search.d.ts +1 -0
- package/dist/clis/github/search.js +20 -0
- package/dist/clis/index.d.ts +13 -0
- package/dist/clis/index.js +16 -0
- package/dist/clis/zhihu/search.d.ts +1 -0
- package/dist/clis/zhihu/search.js +58 -0
- package/dist/engine.d.ts +6 -0
- package/dist/engine.js +77 -0
- package/dist/explore.d.ts +17 -0
- package/dist/explore.js +603 -0
- package/dist/generate.d.ts +11 -0
- package/dist/generate.js +134 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +117 -0
- package/dist/output.d.ts +11 -0
- package/dist/output.js +98 -0
- package/dist/pipeline.d.ts +9 -0
- package/dist/pipeline.js +315 -0
- package/dist/promote.d.ts +1 -0
- package/dist/promote.js +3 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +2 -0
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +42 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +27 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +2 -0
- package/dist/smoke.d.ts +2 -0
- package/dist/smoke.js +2 -0
- package/dist/snapshotFormatter.d.ts +9 -0
- package/dist/snapshotFormatter.js +41 -0
- package/dist/synthesize.d.ts +10 -0
- package/dist/synthesize.js +191 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +73 -0
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +9 -0
- package/package.json +47 -0
- package/src/bilibili.ts +111 -0
- package/src/browser.ts +260 -0
- package/src/clis/bilibili/favorite.ts +42 -0
- package/src/clis/bilibili/feed.ts +71 -0
- package/src/clis/bilibili/history.ts +48 -0
- package/src/clis/bilibili/hot.yaml +38 -0
- package/src/clis/bilibili/me.ts +14 -0
- package/src/clis/bilibili/search.ts +25 -0
- package/src/clis/bilibili/user-videos.ts +42 -0
- package/src/clis/github/search.ts +21 -0
- package/src/clis/github/trending.yaml +58 -0
- package/src/clis/hackernews/top.yaml +36 -0
- package/src/clis/index.ts +19 -0
- package/src/clis/twitter/trending.yaml +40 -0
- package/src/clis/v2ex/hot.yaml +29 -0
- package/src/clis/v2ex/latest.yaml +28 -0
- package/src/clis/zhihu/hot.yaml +28 -0
- package/src/clis/zhihu/search.ts +65 -0
- package/src/engine.ts +86 -0
- package/src/explore.ts +648 -0
- package/src/generate.ts +145 -0
- package/src/main.ts +103 -0
- package/src/output.ts +96 -0
- package/src/pipeline.ts +295 -0
- package/src/promote.ts +3 -0
- package/src/register.ts +2 -0
- package/src/registry.ts +87 -0
- package/src/runtime.ts +36 -0
- package/src/scaffold.ts +2 -0
- package/src/smoke.ts +2 -0
- package/src/snapshotFormatter.ts +51 -0
- package/src/synthesize.ts +210 -0
- package/src/validate.ts +55 -0
- package/src/verify.ts +9 -0
- package/tsconfig.json +17 -0
package/dist/generate.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate: one-shot CLI creation from URL.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the full pipeline:
|
|
5
|
+
* explore (Deep Explore) → synthesize (YAML generation) → register → verify
|
|
6
|
+
*
|
|
7
|
+
* Includes Strategy Cascade: if the initial strategy fails,
|
|
8
|
+
* automatically downgrades and retries.
|
|
9
|
+
*/
|
|
10
|
+
import { exploreUrl } from './explore.js';
|
|
11
|
+
import { synthesizeFromExplore } from './synthesize.js';
|
|
12
|
+
import { registerCandidates } from './register.js';
|
|
13
|
+
const CAPABILITY_ALIASES = {
|
|
14
|
+
search: ['search', '搜索', '查找', 'query', 'keyword'],
|
|
15
|
+
hot: ['hot', '热门', '热榜', '热搜', 'popular', 'top', 'ranking'],
|
|
16
|
+
trending: ['trending', '趋势', '流行', 'discover'],
|
|
17
|
+
feed: ['feed', '动态', '关注', '时间线', 'timeline', 'following'],
|
|
18
|
+
me: ['profile', 'me', '个人信息', 'myinfo', '账号'],
|
|
19
|
+
detail: ['detail', '详情', 'video', 'article', 'view'],
|
|
20
|
+
comments: ['comments', '评论', '回复', 'reply'],
|
|
21
|
+
history: ['history', '历史', '记录'],
|
|
22
|
+
favorite: ['favorite', '收藏', 'bookmark', 'collect'],
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a goal string to a standard capability name.
|
|
26
|
+
*/
|
|
27
|
+
function normalizeGoal(goal) {
|
|
28
|
+
if (!goal)
|
|
29
|
+
return null;
|
|
30
|
+
const lower = goal.trim().toLowerCase();
|
|
31
|
+
for (const [cap, aliases] of Object.entries(CAPABILITY_ALIASES)) {
|
|
32
|
+
if (lower === cap || aliases.some(a => lower.includes(a.toLowerCase())))
|
|
33
|
+
return cap;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Select the best candidate matching the user's goal.
|
|
39
|
+
*/
|
|
40
|
+
function selectCandidate(candidates, goal) {
|
|
41
|
+
if (!candidates.length)
|
|
42
|
+
return null;
|
|
43
|
+
if (!goal)
|
|
44
|
+
return candidates[0]; // highest confidence first
|
|
45
|
+
const normalized = normalizeGoal(goal);
|
|
46
|
+
if (normalized) {
|
|
47
|
+
const exact = candidates.find(c => c.name === normalized);
|
|
48
|
+
if (exact)
|
|
49
|
+
return exact;
|
|
50
|
+
}
|
|
51
|
+
const lower = (goal ?? '').trim().toLowerCase();
|
|
52
|
+
const partial = candidates.find(c => c.name?.toLowerCase().includes(lower) || lower.includes(c.name?.toLowerCase()));
|
|
53
|
+
return partial ?? candidates[0];
|
|
54
|
+
}
|
|
55
|
+
export async function generateCliFromUrl(opts) {
|
|
56
|
+
// Step 1: Deep Explore
|
|
57
|
+
const exploreResult = await exploreUrl(opts.url, {
|
|
58
|
+
BrowserFactory: opts.BrowserFactory,
|
|
59
|
+
site: opts.site,
|
|
60
|
+
goal: normalizeGoal(opts.goal) ?? opts.goal,
|
|
61
|
+
waitSeconds: opts.waitSeconds ?? 3,
|
|
62
|
+
});
|
|
63
|
+
// Step 2: Synthesize candidates
|
|
64
|
+
const synthesizeResult = synthesizeFromExplore(exploreResult.out_dir, {
|
|
65
|
+
top: opts.top ?? 5,
|
|
66
|
+
});
|
|
67
|
+
// Step 3: Select best candidate for goal
|
|
68
|
+
const selected = selectCandidate(synthesizeResult.candidates ?? [], opts.goal);
|
|
69
|
+
const selectedSite = selected?.site ?? synthesizeResult.site ?? exploreResult.site;
|
|
70
|
+
// Step 4: Register (if requested)
|
|
71
|
+
let registerResult = null;
|
|
72
|
+
if (opts.register !== false && synthesizeResult.candidate_count > 0) {
|
|
73
|
+
try {
|
|
74
|
+
registerResult = registerCandidates({
|
|
75
|
+
target: synthesizeResult.out_dir,
|
|
76
|
+
builtinClis: opts.builtinClis,
|
|
77
|
+
userClis: opts.userClis,
|
|
78
|
+
name: selected?.name,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
}
|
|
83
|
+
const ok = exploreResult.endpoint_count > 0 && synthesizeResult.candidate_count > 0;
|
|
84
|
+
return {
|
|
85
|
+
ok,
|
|
86
|
+
goal: opts.goal,
|
|
87
|
+
normalized_goal: normalizeGoal(opts.goal),
|
|
88
|
+
site: selectedSite,
|
|
89
|
+
selected_candidate: selected,
|
|
90
|
+
selected_command: selected ? `${selectedSite}/${selected.name}` : '(none)',
|
|
91
|
+
explore: {
|
|
92
|
+
endpoint_count: exploreResult.endpoint_count,
|
|
93
|
+
api_endpoint_count: exploreResult.api_endpoint_count,
|
|
94
|
+
capability_count: exploreResult.capabilities?.length ?? 0,
|
|
95
|
+
top_strategy: exploreResult.top_strategy,
|
|
96
|
+
framework: exploreResult.framework,
|
|
97
|
+
},
|
|
98
|
+
synthesize: {
|
|
99
|
+
candidate_count: synthesizeResult.candidate_count,
|
|
100
|
+
candidates: (synthesizeResult.candidates ?? []).map((c) => ({
|
|
101
|
+
name: c.name,
|
|
102
|
+
strategy: c.strategy,
|
|
103
|
+
confidence: c.confidence,
|
|
104
|
+
})),
|
|
105
|
+
},
|
|
106
|
+
register: registerResult,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function renderGenerateSummary(r) {
|
|
110
|
+
const lines = [
|
|
111
|
+
`opencli generate: ${r.ok ? 'OK' : 'FAIL'}`,
|
|
112
|
+
`Site: ${r.site}`,
|
|
113
|
+
`Goal: ${r.goal ?? '(auto)'}`,
|
|
114
|
+
`Selected: ${r.selected_command}`,
|
|
115
|
+
'',
|
|
116
|
+
`Explore:`,
|
|
117
|
+
` Endpoints: ${r.explore?.endpoint_count ?? 0} total, ${r.explore?.api_endpoint_count ?? 0} API`,
|
|
118
|
+
` Capabilities: ${r.explore?.capability_count ?? 0}`,
|
|
119
|
+
` Strategy: ${r.explore?.top_strategy ?? 'unknown'}`,
|
|
120
|
+
'',
|
|
121
|
+
`Synthesize:`,
|
|
122
|
+
` Candidates: ${r.synthesize?.candidate_count ?? 0}`,
|
|
123
|
+
];
|
|
124
|
+
for (const c of r.synthesize?.candidates ?? []) {
|
|
125
|
+
lines.push(` • ${c.name} (${c.strategy}, ${((c.confidence ?? 0) * 100).toFixed(0)}%)`);
|
|
126
|
+
}
|
|
127
|
+
if (r.register)
|
|
128
|
+
lines.push(`\nRegistered: ${r.register.count ?? 0}`);
|
|
129
|
+
const fw = r.explore?.framework ?? {};
|
|
130
|
+
const fwNames = Object.entries(fw).filter(([, v]) => v).map(([k]) => k);
|
|
131
|
+
if (fwNames.length)
|
|
132
|
+
lines.push(`Framework: ${fwNames.join(', ')}`);
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* opencli — Make any website your CLI. AI-powered.
|
|
4
|
+
*/
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { discoverClis, executeCommand } from './engine.js';
|
|
11
|
+
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
12
|
+
import { render as renderOutput } from './output.js';
|
|
13
|
+
import './clis/index.js';
|
|
14
|
+
import { PlaywrightMCP } from './browser.js';
|
|
15
|
+
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
|
|
19
|
+
const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
20
|
+
discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version('0.1.0');
|
|
23
|
+
// ── Built-in commands ──────────────────────────────────────────────────────
|
|
24
|
+
program.command('list').description('List all available CLI commands').option('--json', 'JSON output')
|
|
25
|
+
.action((opts) => {
|
|
26
|
+
const registry = getRegistry();
|
|
27
|
+
const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b)));
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
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));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const sites = new Map();
|
|
33
|
+
for (const cmd of commands) {
|
|
34
|
+
const g = sites.get(cmd.site) ?? [];
|
|
35
|
+
g.push(cmd);
|
|
36
|
+
sites.set(cmd.site, g);
|
|
37
|
+
}
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.bold(' opencli') + chalk.dim(' — available commands'));
|
|
40
|
+
console.log();
|
|
41
|
+
for (const [site, cmds] of sites) {
|
|
42
|
+
console.log(chalk.bold.cyan(` ${site}`));
|
|
43
|
+
for (const cmd of cmds) {
|
|
44
|
+
const tag = strategyLabel(cmd) === 'public' ? chalk.green('[public]') : chalk.yellow(`[${strategyLabel(cmd)}]`);
|
|
45
|
+
console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`);
|
|
46
|
+
}
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk.dim(` ${commands.length} commands across ${sites.size} sites`));
|
|
50
|
+
console.log();
|
|
51
|
+
});
|
|
52
|
+
program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name')
|
|
53
|
+
.action(async (target) => { const { validateClisWithTarget, renderValidationReport } = await import('./validate.js'); console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target))); });
|
|
54
|
+
program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false)
|
|
55
|
+
.action(async (target, opts) => { const { verifyClis, renderVerifyReport } = await import('./verify.js'); const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke }); console.log(renderVerifyReport(r)); process.exitCode = r.ok ? 0 : 1; });
|
|
56
|
+
program.command('explore').description('Explore a website').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3')
|
|
57
|
+
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait) }))); });
|
|
58
|
+
program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
|
|
59
|
+
.action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
|
|
60
|
+
program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
|
|
61
|
+
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: PlaywrightMCP, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
|
|
62
|
+
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
63
|
+
const registry = getRegistry();
|
|
64
|
+
const siteGroups = new Map();
|
|
65
|
+
for (const [, cmd] of registry) {
|
|
66
|
+
let siteCmd = siteGroups.get(cmd.site);
|
|
67
|
+
if (!siteCmd) {
|
|
68
|
+
siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
|
|
69
|
+
siteGroups.set(cmd.site, siteCmd);
|
|
70
|
+
}
|
|
71
|
+
const subCmd = siteCmd.command(cmd.name).description(cmd.description);
|
|
72
|
+
for (const arg of cmd.args) {
|
|
73
|
+
const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
74
|
+
if (arg.required)
|
|
75
|
+
subCmd.requiredOption(flag, arg.help ?? '');
|
|
76
|
+
else if (arg.default != null)
|
|
77
|
+
subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
78
|
+
else
|
|
79
|
+
subCmd.option(flag, arg.help ?? '');
|
|
80
|
+
}
|
|
81
|
+
subCmd.option('-f, --format <fmt>', 'Output format: table, json, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
|
|
82
|
+
subCmd.action(async (actionOpts) => {
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
const kwargs = {};
|
|
85
|
+
for (const arg of cmd.args) {
|
|
86
|
+
const v = actionOpts[arg.name];
|
|
87
|
+
if (v !== undefined)
|
|
88
|
+
kwargs[arg.name] = coerce(v, arg.type ?? 'str');
|
|
89
|
+
else if (arg.default != null)
|
|
90
|
+
kwargs[arg.name] = arg.default;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
let result;
|
|
94
|
+
if (cmd.browser) {
|
|
95
|
+
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
|
|
99
|
+
}
|
|
100
|
+
renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error(chalk.red(`Error: ${err.message ?? err}`));
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function coerce(v, t) {
|
|
109
|
+
if (t === 'bool')
|
|
110
|
+
return ['1', 'true', 'yes', 'on'].includes(String(v).toLowerCase());
|
|
111
|
+
if (t === 'int')
|
|
112
|
+
return parseInt(String(v), 10);
|
|
113
|
+
if (t === 'float')
|
|
114
|
+
return parseFloat(String(v));
|
|
115
|
+
return String(v);
|
|
116
|
+
}
|
|
117
|
+
program.parse();
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting: table, JSON, Markdown, CSV.
|
|
3
|
+
*/
|
|
4
|
+
export interface RenderOptions {
|
|
5
|
+
fmt?: string;
|
|
6
|
+
columns?: string[];
|
|
7
|
+
title?: string;
|
|
8
|
+
elapsed?: number;
|
|
9
|
+
source?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function render(data: any, opts?: RenderOptions): void;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting: table, JSON, Markdown, CSV.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import Table from 'cli-table3';
|
|
6
|
+
export function render(data, opts = {}) {
|
|
7
|
+
const fmt = opts.fmt ?? 'table';
|
|
8
|
+
if (data === null || data === undefined) {
|
|
9
|
+
console.log(data);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
switch (fmt) {
|
|
13
|
+
case 'json':
|
|
14
|
+
renderJson(data);
|
|
15
|
+
break;
|
|
16
|
+
case 'md':
|
|
17
|
+
case 'markdown':
|
|
18
|
+
renderMarkdown(data, opts);
|
|
19
|
+
break;
|
|
20
|
+
case 'csv':
|
|
21
|
+
renderCsv(data, opts);
|
|
22
|
+
break;
|
|
23
|
+
default:
|
|
24
|
+
renderTable(data, opts);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function renderTable(data, opts) {
|
|
29
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
30
|
+
if (!rows.length) {
|
|
31
|
+
console.log(chalk.dim('(no data)'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const columns = opts.columns ?? Object.keys(rows[0]);
|
|
35
|
+
const header = columns.map((c, i) => i === 0 ? '#' : capitalize(c));
|
|
36
|
+
const table = new Table({
|
|
37
|
+
head: header.map(h => chalk.bold(h)),
|
|
38
|
+
style: { head: [], border: [] },
|
|
39
|
+
wordWrap: true,
|
|
40
|
+
wrapOnWordBoundary: true,
|
|
41
|
+
colWidths: columns.map((c, i) => {
|
|
42
|
+
if (i === 0)
|
|
43
|
+
return 4;
|
|
44
|
+
if (c === 'url' || c === 'description')
|
|
45
|
+
return null;
|
|
46
|
+
if (c === 'title' || c === 'name' || c === 'repo')
|
|
47
|
+
return null;
|
|
48
|
+
return null;
|
|
49
|
+
}).filter(() => true),
|
|
50
|
+
});
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
table.push(columns.map(c => {
|
|
53
|
+
const v = row[c];
|
|
54
|
+
return v === null || v === undefined ? '' : String(v);
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
if (opts.title)
|
|
59
|
+
console.log(chalk.dim(` ${opts.title}`));
|
|
60
|
+
console.log(table.toString());
|
|
61
|
+
const footer = [];
|
|
62
|
+
footer.push(`${rows.length} items`);
|
|
63
|
+
if (opts.elapsed)
|
|
64
|
+
footer.push(`${opts.elapsed.toFixed(1)}s`);
|
|
65
|
+
if (opts.source)
|
|
66
|
+
footer.push(opts.source);
|
|
67
|
+
console.log(chalk.dim(footer.join(' · ')));
|
|
68
|
+
}
|
|
69
|
+
function renderJson(data) {
|
|
70
|
+
console.log(JSON.stringify(data, null, 2));
|
|
71
|
+
}
|
|
72
|
+
function renderMarkdown(data, opts) {
|
|
73
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
74
|
+
if (!rows.length)
|
|
75
|
+
return;
|
|
76
|
+
const columns = opts.columns ?? Object.keys(rows[0]);
|
|
77
|
+
console.log('| ' + columns.join(' | ') + ' |');
|
|
78
|
+
console.log('| ' + columns.map(() => '---').join(' | ') + ' |');
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
console.log('| ' + columns.map(c => String(row[c] ?? '')).join(' | ') + ' |');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function renderCsv(data, opts) {
|
|
84
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
85
|
+
if (!rows.length)
|
|
86
|
+
return;
|
|
87
|
+
const columns = opts.columns ?? Object.keys(rows[0]);
|
|
88
|
+
console.log(columns.join(','));
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
console.log(columns.map(c => {
|
|
91
|
+
const v = String(row[c] ?? '');
|
|
92
|
+
return v.includes(',') || v.includes('"') ? `"${v.replace(/"/g, '""')}"` : v;
|
|
93
|
+
}).join(','));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function capitalize(s) {
|
|
97
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
98
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML pipeline executor.
|
|
3
|
+
* Steps: fetch, navigate, evaluate, map, filter, sort, limit, select, snapshot, click, type, wait, press, intercept.
|
|
4
|
+
*/
|
|
5
|
+
export interface PipelineContext {
|
|
6
|
+
args?: Record<string, any>;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function executePipeline(page: any, pipeline: any[], ctx?: PipelineContext): Promise<any>;
|